diff --git a/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml b/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml index 148f913d..01c84e73 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml @@ -52,7 +52,7 @@ presets: # Creates an array in the form of [ { url: ..., category: ..., metadata_field_1: ... }, ... ] 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 { : { category: ..., metadata_field_1: ... }, ... } category_url_map: >- diff --git a/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml b/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml index 8104781c..e292deea 100644 --- a/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml +++ b/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml @@ -53,6 +53,6 @@ presets: - "_music_video_extras_base" - "_music_video_tags" - overrides: - metadata_verify_plex_suffix: "{url_metadata}" - music_video_file_name_suffix: "-{music_video_album}" \ No newline at end of file +# overrides: +# metadata_verify_plex_suffix: "{url_metadata}" +# music_video_file_name_suffix: "-{music_video_album}" \ No newline at end of file diff --git a/src/ytdl_sub/script/functions/conditional_functions.py b/src/ytdl_sub/script/functions/conditional_functions.py index 8b9da74b..429fdbd5 100644 --- a/src/ytdl_sub/script/functions/conditional_functions.py +++ b/src/ytdl_sub/script/functions/conditional_functions.py @@ -18,8 +18,8 @@ class ConditionalFunctions: depending on the ``condition`` value. """ if condition.value: - return true - return false + return true.value() + return false.value() @staticmethod def elif_(*if_elif_else: AnyArgument) -> AnyArgument: @@ -50,9 +50,9 @@ class ConditionalFunctions: for idx in range(0, len(arguments) - 1, 2): if bool(arguments[idx].value): - return arguments[idx + 1] + return arguments[idx + 1].value() - return arguments[-1] + return arguments[-1].value() @staticmethod def if_passthrough( @@ -63,6 +63,7 @@ class ConditionalFunctions: Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True, otherwise returns ``else_arg``. """ - if bool(maybe_true_arg.value): - return maybe_true_arg - return else_arg + maybe_true_value = maybe_true_arg.value() + if bool(maybe_true_value): + return maybe_true_value + return else_arg.value() diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index 6f095df9..27b4b1e9 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -7,7 +7,7 @@ from typing import Set from ytdl_sub.script.functions import Functions from ytdl_sub.script.parser import parse 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.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import FunctionArgument @@ -146,20 +146,34 @@ class Script: self, prefix: str, name: str, definition: SyntaxTree ): 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): return - lambda_function_names = set( - lamb.value - for lamb in SyntaxTree(function.args).lambdas - if isinstance(lamb, Lambda) and lamb in function.args - ) + lambda_function_names: Set[str] = set() + for lamb in SyntaxTree(function.args).lambdas: + if 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 for lambda_function_name in lambda_function_names: 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()): expected_args_str = str(lambda_spec.num_required_args) if lambda_spec.num_required_args != len(lambda_spec.args): diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 30193e1a..e167774f 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -117,7 +117,7 @@ class BuiltInFunction(Function, BuiltInFunctionType): ------- The FunctionSpec of the BuiltInFunction """ - return FunctionSpec.from_callable(self.callable) + return FunctionSpec.from_callable(name=self.name, callable_ref=self.callable) @classmethod def _arg_output_type(cls, arg: Argument) -> Type[Argument]: @@ -257,14 +257,27 @@ class BuiltInFunction(Function, BuiltInFunctionType): resolved_variables: Dict[Variable, Resolvable], custom_functions: Dict[str, "VariableDependency"], ) -> 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 - resolved_arguments: List[Resolvable | Lambda] = [ - self._resolve_argument_type( - arg=arg, - resolved_variables=resolved_variables, - custom_functions=custom_functions, + resolved_arguments: List[Resolvable | Lambda | ReturnableArgument] = [ + ( + self._resolve_argument_type( + arg=arg, + resolved_variables=resolved_variables, + custom_functions=custom_functions, + ) + if idx not in conditional_return_args + else ReturnableArgument( + value=functools.partial( + self._resolve_argument_type, arg, resolved_variables, custom_functions + ) + ) ) - for arg in self.args + for idx, arg in enumerate(self.args) ] # If a lambda is in a function's arg, resolve it differently @@ -283,8 +296,6 @@ class BuiltInFunction(Function, BuiltInFunctionType): custom_functions=custom_functions, ) - # TODO: Make conditionals not execute all branches!!! - try: return self.callable(*resolved_arguments) except (UserThrownRuntimeError, RuntimeException): diff --git a/src/ytdl_sub/script/types/variable_dependency.py b/src/ytdl_sub/script/types/variable_dependency.py index f7ab83cd..ec7be6d5 100644 --- a/src/ytdl_sub/script/types/variable_dependency.py +++ b/src/ytdl_sub/script/types/variable_dependency.py @@ -34,17 +34,21 @@ class VariableDependency(ABC): 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] = [] for arg in self._iterable_arguments: if subclass and issubclass(type(arg), ttype): output.append(arg) - elif isinstance(arg, ttype): + elif instance and isinstance(arg, ttype): + output.append(arg) + elif type(arg) == ttype: output.append(arg) if isinstance(arg, VariableDependency): # pylint: disable=protected-access - output.extend(arg._recurse_get(ttype)) + output.extend(arg._recurse_get(ttype, subclass=subclass, instance=instance)) # pylint: enable=protected-access return output @@ -57,7 +61,7 @@ class VariableDependency(ABC): ------- All Variables that this depends on. """ - return set(self._recurse_get(Variable)) + return set(self._recurse_get(Variable, instance=False)) @final @property diff --git a/src/ytdl_sub/script/utils/type_checking.py b/src/ytdl_sub/script/utils/type_checking.py index 0c3426ff..856e75e4 100644 --- a/src/ytdl_sub/script/utils/type_checking.py +++ b/src/ytdl_sub/script/utils/type_checking.py @@ -116,6 +116,7 @@ def is_type_compatible( @dataclass(frozen=True) class FunctionSpec: + function_name: str return_type: Type[Resolvable] arg_names: List[str] 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 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 def is_lambda_reduce_function(self) -> Optional[Type[LambdaReduce]]: """ @@ -259,7 +276,7 @@ class FunctionSpec: return self._to_human_readable_name(self.return_type) @classmethod - def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec": + def from_callable(cls, name: str, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec": """ Returns ------- @@ -268,12 +285,14 @@ class FunctionSpec: arg_spec: FullArgSpec = inspect.getfullargspec(callable_ref) if arg_spec.varargs: return FunctionSpec( + function_name=name, return_type=arg_spec.annotations["return"], arg_names=[arg_spec.varargs], varargs=arg_spec.annotations[arg_spec.varargs], ) return FunctionSpec( + function_name=name, return_type=arg_spec.annotations["return"], arg_names=arg_spec.args, args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args], diff --git a/tests/unit/script/types/test_lambda_function.py b/tests/unit/script/types/test_lambda_function.py index 379c2207..b69bc6e3 100644 --- a/tests/unit/script/types/test_lambda_function.py +++ b/tests/unit/script/types/test_lambda_function.py @@ -54,6 +54,23 @@ class TestLambdaFunction: } ).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: @pytest.mark.parametrize( diff --git a/tools/docgen/scripting_functions.py b/tools/docgen/scripting_functions.py index c4663477..cc3d4eb3 100644 --- a/tools/docgen/scripting_functions.py +++ b/tools/docgen/scripting_functions.py @@ -30,7 +30,7 @@ def function_class_to_name(obj: Type[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 += display_function_name out += spec.human_readable_input_args()