kinda working

This commit is contained in:
Jesse Bannon 2024-06-03 11:44:22 -07:00
parent f22a072ff2
commit 13a37b6453
9 changed files with 100 additions and 34 deletions

View file

@ -52,7 +52,7 @@ presets:
# Creates an array in the form of [ { url: ..., category: ..., metadata_field_1: ... }, ... ] # Creates an array in the form of [ { url: ..., category: ..., metadata_field_1: ... }, ... ]
category_url_array: >- category_url_array: >-
{ %map_apply( subscription_dict, %flat_array__category_to_map_format ) } { %array_flatten( %map_apply( subscription_dict, %flat_array__category_to_map_format ) ) }
# Creates a map in the form of { <url value>: { category: ..., metadata_field_1: ... }, ... } # Creates a map in the form of { <url value>: { category: ..., metadata_field_1: ... }, ... }
category_url_map: >- category_url_map: >-

View file

@ -53,6 +53,6 @@ presets:
- "_music_video_extras_base" - "_music_video_extras_base"
- "_music_video_tags" - "_music_video_tags"
overrides: # overrides:
metadata_verify_plex_suffix: "{url_metadata}" # metadata_verify_plex_suffix: "{url_metadata}"
music_video_file_name_suffix: "-{music_video_album}" # music_video_file_name_suffix: "-{music_video_album}"

View file

@ -18,8 +18,8 @@ class ConditionalFunctions:
depending on the ``condition`` value. depending on the ``condition`` value.
""" """
if condition.value: if condition.value:
return true return true.value()
return false return false.value()
@staticmethod @staticmethod
def elif_(*if_elif_else: AnyArgument) -> AnyArgument: def elif_(*if_elif_else: AnyArgument) -> AnyArgument:
@ -50,9 +50,9 @@ class ConditionalFunctions:
for idx in range(0, len(arguments) - 1, 2): for idx in range(0, len(arguments) - 1, 2):
if bool(arguments[idx].value): if bool(arguments[idx].value):
return arguments[idx + 1] return arguments[idx + 1].value()
return arguments[-1] return arguments[-1].value()
@staticmethod @staticmethod
def if_passthrough( def if_passthrough(
@ -63,6 +63,7 @@ class ConditionalFunctions:
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True, Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
otherwise returns ``else_arg``. otherwise returns ``else_arg``.
""" """
if bool(maybe_true_arg.value): maybe_true_value = maybe_true_arg.value()
return maybe_true_arg if bool(maybe_true_value):
return else_arg return maybe_true_value
return else_arg.value()

View file

@ -7,7 +7,7 @@ from typing import Set
from ytdl_sub.script.functions import Functions from ytdl_sub.script.functions import Functions
from ytdl_sub.script.parser import parse from ytdl_sub.script.parser import parse
from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.script_output import ScriptOutput
from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import Lambda, BuiltInFunctionType
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
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 FunctionArgument from ytdl_sub.script.types.variable import FunctionArgument
@ -146,20 +146,34 @@ class Script:
self, prefix: str, name: str, definition: SyntaxTree self, prefix: str, name: str, definition: SyntaxTree
): ):
for function in definition.built_in_functions: for function in definition.built_in_functions:
spec = FunctionSpec.from_callable(Functions.get(function.name)) for arg in function.args:
self._ensure_lambda_usage_num_input_arguments_valid(
prefix=prefix, name=name, definition=SyntaxTree([arg])
)
spec = FunctionSpec.from_callable(
name=function.name, callable_ref=Functions.get(function.name)
)
if not (lambda_type := spec.is_lambda_like): if not (lambda_type := spec.is_lambda_like):
return return
lambda_function_names = set( lambda_function_names: Set[str] = set()
lamb.value for lamb in SyntaxTree(function.args).lambdas:
for lamb in SyntaxTree(function.args).lambdas if lamb in function.args:
if isinstance(lamb, Lambda) and lamb in function.args lambda_function_names.add(lamb.value)
)
# See if the arg outputs a lambda (from an if).
# If so, add the possible lambda to be checked
for arg in function.args:
if isinstance(arg, BuiltInFunctionType) and arg.output_type() == Lambda and lamb in arg.args:
lambda_function_names.add(lamb.value)
# Only case len(lambda_function_names) > 1 is when used in if-statements # Only case len(lambda_function_names) > 1 is when used in if-statements
for lambda_function_name in lambda_function_names: for lambda_function_name in lambda_function_names:
if Functions.is_built_in(lambda_function_name): if Functions.is_built_in(lambda_function_name):
lambda_spec = FunctionSpec.from_callable(Functions.get(lambda_function_name)) lambda_spec = FunctionSpec.from_callable(
name=lambda_function_name, callable_ref=Functions.get(lambda_function_name)
)
if not lambda_spec.is_num_args_compatible(lambda_type.num_input_args()): if not lambda_spec.is_num_args_compatible(lambda_type.num_input_args()):
expected_args_str = str(lambda_spec.num_required_args) expected_args_str = str(lambda_spec.num_required_args)
if lambda_spec.num_required_args != len(lambda_spec.args): if lambda_spec.num_required_args != len(lambda_spec.args):

View file

@ -117,7 +117,7 @@ class BuiltInFunction(Function, BuiltInFunctionType):
------- -------
The FunctionSpec of the BuiltInFunction The FunctionSpec of the BuiltInFunction
""" """
return FunctionSpec.from_callable(self.callable) return FunctionSpec.from_callable(name=self.name, callable_ref=self.callable)
@classmethod @classmethod
def _arg_output_type(cls, arg: Argument) -> Type[Argument]: def _arg_output_type(cls, arg: Argument) -> Type[Argument]:
@ -257,14 +257,27 @@ class BuiltInFunction(Function, BuiltInFunctionType):
resolved_variables: Dict[Variable, Resolvable], resolved_variables: Dict[Variable, Resolvable],
custom_functions: Dict[str, "VariableDependency"], custom_functions: Dict[str, "VariableDependency"],
) -> Resolvable: ) -> Resolvable:
# TODO: Make conditionals not execute all branches!!!
conditional_return_args = self.function_spec.conditional_arg_indices(
num_input_args=len(self.args)
)
# Resolve all non-lambda arguments # Resolve all non-lambda arguments
resolved_arguments: List[Resolvable | Lambda] = [ resolved_arguments: List[Resolvable | Lambda | ReturnableArgument] = [
(
self._resolve_argument_type( self._resolve_argument_type(
arg=arg, arg=arg,
resolved_variables=resolved_variables, resolved_variables=resolved_variables,
custom_functions=custom_functions, custom_functions=custom_functions,
) )
for arg in self.args if idx not in conditional_return_args
else ReturnableArgument(
value=functools.partial(
self._resolve_argument_type, arg, resolved_variables, custom_functions
)
)
)
for idx, arg in enumerate(self.args)
] ]
# If a lambda is in a function's arg, resolve it differently # If a lambda is in a function's arg, resolve it differently
@ -283,8 +296,6 @@ class BuiltInFunction(Function, BuiltInFunctionType):
custom_functions=custom_functions, custom_functions=custom_functions,
) )
# TODO: Make conditionals not execute all branches!!!
try: try:
return self.callable(*resolved_arguments) return self.callable(*resolved_arguments)
except (UserThrownRuntimeError, RuntimeException): except (UserThrownRuntimeError, RuntimeException):

View file

@ -34,17 +34,21 @@ class VariableDependency(ABC):
Any arguments in the VariableDependency that may or may not need to be resolved. Any arguments in the VariableDependency that may or may not need to be resolved.
""" """
def _recurse_get(self, ttype: Type[TypeT], subclass: bool = False) -> List[TypeT]: def _recurse_get(
self, ttype: Type[TypeT], subclass: bool = False, instance: bool = True
) -> List[TypeT]:
output: List[TypeT] = [] output: List[TypeT] = []
for arg in self._iterable_arguments: for arg in self._iterable_arguments:
if subclass and issubclass(type(arg), ttype): if subclass and issubclass(type(arg), ttype):
output.append(arg) output.append(arg)
elif isinstance(arg, ttype): elif instance and isinstance(arg, ttype):
output.append(arg)
elif type(arg) == ttype:
output.append(arg) output.append(arg)
if isinstance(arg, VariableDependency): if isinstance(arg, VariableDependency):
# pylint: disable=protected-access # pylint: disable=protected-access
output.extend(arg._recurse_get(ttype)) output.extend(arg._recurse_get(ttype, subclass=subclass, instance=instance))
# pylint: enable=protected-access # pylint: enable=protected-access
return output return output
@ -57,7 +61,7 @@ class VariableDependency(ABC):
------- -------
All Variables that this depends on. All Variables that this depends on.
""" """
return set(self._recurse_get(Variable)) return set(self._recurse_get(Variable, instance=False))
@final @final
@property @property

View file

@ -116,6 +116,7 @@ def is_type_compatible(
@dataclass(frozen=True) @dataclass(frozen=True)
class FunctionSpec: class FunctionSpec:
function_name: str
return_type: Type[Resolvable] return_type: Type[Resolvable]
arg_names: List[str] arg_names: List[str]
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
@ -185,6 +186,22 @@ class FunctionSpec:
return sum(1 for arg in self.args if not is_optional(arg)) return sum(1 for arg in self.args if not is_optional(arg))
return 0 # varargs can take any number return 0 # varargs can take any number
def conditional_arg_indices(self, num_input_args: int) -> List[int]:
"""
Returns
-------
If the function is conditional, return the indices of the arguments that
return for different branches.
"""
if self.function_name == "if":
return [1, 2] # true, false
if self.function_name == "elif":
# if, retA, elif, retB, retElse
return list(range(1, num_input_args, 2)) + [num_input_args - 1]
if self.function_name == "if_passthrough":
return [0, 1] # true-passthrough, false-passthrough
return []
@property @property
def is_lambda_reduce_function(self) -> Optional[Type[LambdaReduce]]: def is_lambda_reduce_function(self) -> Optional[Type[LambdaReduce]]:
""" """
@ -259,7 +276,7 @@ class FunctionSpec:
return self._to_human_readable_name(self.return_type) return self._to_human_readable_name(self.return_type)
@classmethod @classmethod
def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec": def from_callable(cls, name: str, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec":
""" """
Returns Returns
------- -------
@ -268,12 +285,14 @@ class FunctionSpec:
arg_spec: FullArgSpec = inspect.getfullargspec(callable_ref) arg_spec: FullArgSpec = inspect.getfullargspec(callable_ref)
if arg_spec.varargs: if arg_spec.varargs:
return FunctionSpec( return FunctionSpec(
function_name=name,
return_type=arg_spec.annotations["return"], return_type=arg_spec.annotations["return"],
arg_names=[arg_spec.varargs], arg_names=[arg_spec.varargs],
varargs=arg_spec.annotations[arg_spec.varargs], varargs=arg_spec.annotations[arg_spec.varargs],
) )
return FunctionSpec( return FunctionSpec(
function_name=name,
return_type=arg_spec.annotations["return"], return_type=arg_spec.annotations["return"],
arg_names=arg_spec.args, arg_names=arg_spec.args,
args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args], args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args],

View file

@ -54,6 +54,23 @@ class TestLambdaFunction:
} }
).resolve() == ScriptOutput({"output": Integer(4)}) ).resolve() == ScriptOutput({"output": Integer(4)})
def test_multiple_lambdas_single_definition(self):
url_map_def = """{
%array_reduce(
%array_apply( array_def, %array_map_format),
%map_extend
)
}"""
script = Script(
{
"%array_map_format": "{ {$0: $0 } }",
"array_def": "{ [1, 2, 3] }",
"category_url_map": url_map_def,
}
)
assert script.resolve().get("category_url_map").native == {1: 1, 2: 2, 3: 3}
class TestLambdaFunctionIncompatibleNumArguments: class TestLambdaFunctionIncompatibleNumArguments:
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -30,7 +30,7 @@ def function_class_to_name(obj: Type[Any]) -> str:
def function_type_hinting(display_function_name: str, function: Any) -> str: def function_type_hinting(display_function_name: str, function: Any) -> str:
spec = FunctionSpec.from_callable(function) spec = FunctionSpec.from_callable(name=display_function_name, callable_ref=function)
out = ":spec: ``" out = ":spec: ``"
out += display_function_name out += display_function_name
out += spec.human_readable_input_args() out += spec.human_readable_input_args()