From 873ecc01475fc11ac201d4a48ce03b02f460d935 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Wed, 20 Dec 2023 23:46:17 -0800 Subject: [PATCH] [BUGFIX] Fix recursion limit when applying large reduce functions (#851) Python would think a recursive error occurred, but in reality, the stack got too large due to poor optimization when executing array reduce functions. Thanks Melissa from Discord for the bug report! --- src/ytdl_sub/script/types/function.py | 27 +++++++------ src/ytdl_sub/utils/scriptable.py | 12 +++--- .../script/functions/test_array_functions.py | 39 +++++++++++++++++++ 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 94bbad5d..7d1cea85 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -232,20 +232,25 @@ class BuiltInFunction(Function, BuiltInFunctionType): if len(lambda_array.value) == 1: return lambda_array.value[0] - reduced = self._instantiate_lambda( - lambda_function_name=lambda_function_name, - args=[lambda_array.value[0], lambda_array.value[1]], - ) - for idx in range(2, len(lambda_array.value)): - reduced = self._instantiate_lambda( - lambda_function_name=lambda_function_name, args=[reduced, lambda_array.value[idx]] - ) - - return self._resolve_argument_type( - arg=reduced, + reduced: Resolvable = self._resolve_argument_type( + arg=self._instantiate_lambda( + lambda_function_name=lambda_function_name, + args=[lambda_array.value[0], lambda_array.value[1]], + ), resolved_variables=resolved_variables, custom_functions=custom_functions, ) + for idx in range(2, len(lambda_array.value)): + reduced = self._resolve_argument_type( + arg=self._instantiate_lambda( + lambda_function_name=lambda_function_name, + args=[reduced, lambda_array.value[idx]], + ), + resolved_variables=resolved_variables, + custom_functions=custom_functions, + ) + + return reduced def resolve( self, diff --git a/src/ytdl_sub/utils/scriptable.py b/src/ytdl_sub/utils/scriptable.py index 0dace721..105bce98 100644 --- a/src/ytdl_sub/utils/scriptable.py +++ b/src/ytdl_sub/utils/scriptable.py @@ -19,12 +19,14 @@ class Scriptable(ABC): Shared class between Entry and Overrides to manage their underlying Script. """ - def __init__(self): - self.script = Script( - ScriptUtils.add_sanitized_variables( - dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS)) - ) + _BASE_SCRIPT: Script = Script( + ScriptUtils.add_sanitized_variables( + dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS)) ) + ) + + def __init__(self): + self.script = copy.deepcopy(Scriptable._BASE_SCRIPT) self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES) def update_script(self) -> None: diff --git a/tests/unit/script/functions/test_array_functions.py b/tests/unit/script/functions/test_array_functions.py index d9515d2d..06a3b150 100644 --- a/tests/unit/script/functions/test_array_functions.py +++ b/tests/unit/script/functions/test_array_functions.py @@ -58,6 +58,45 @@ class TestArrayFunctions: output = single_variable_output("{%array_reduce([1, 2, 3, 4], %add)}") assert output == 10 + def test_array_reduce_complex(self): + output = ( + Script( + { + "%custom_get": """{ + %if( + %bool(siblings_array), + %array_apply_fixed( + siblings_array, + %string($0), + %map_get + ) + [] + ) + }""", + "siblings_array": """{ + [ + {'upload_date': '20200101'}, + {'upload_date': '19940101'} + ] + }""", + "upload_date": "20230101", + "output": """{ + %array_reduce( + %if_passthrough( + %custom_get('upload_date'), + [ upload_date ] + ), + %max + ) + }""", + } + ) + .resolve(update=True) + .get("output") + .native + ) + assert output == "20200101" + def test_array_enumerate(self): output = ( Script(