From 869fc0b836a7bb9ad3e790d29209e7f5d72d8796 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Wed, 31 Dec 2025 08:22:26 -0800 Subject: [PATCH] [BUGFIX] Fix custom function lambda cycle detection (#1404) Cycles detection did not take into account custom functions' variable usage. This is now fixed. --- src/ytdl_sub/script/script.py | 16 ++++++++++++++++ src/ytdl_sub/script/types/resolvable.py | 8 ++++++++ .../script/types/variable_dependency.py | 17 ++++++++++++++--- tests/unit/script/types/test_lambda_function.py | 8 ++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index b4c431df..38e0346e 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -66,6 +66,22 @@ class Script: deps=deps + [dep.name], ) + for custom_func in variable_dependency.custom_function_dependencies( + custom_function_definitions=self._functions + ): + for dep in self._functions[custom_func.name].variables: + self._ensure_no_cycle( + name=variable_name, + dep=dep.name, + deps=deps + [custom_func.definition_name()], + definitions=self._variables, + ) + self._traverse_variable_dependencies( + variable_name=variable_name, + variable_dependency=self._variables[dep.name], + deps=deps + [custom_func.definition_name(), dep.name], + ) + def _ensure_no_variable_cycles(self, variables: Dict[str, SyntaxTree]): for variable_name, variable_definition in variables.items(): self._traverse_variable_dependencies( diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index c042b1f5..ad9ff300 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -193,6 +193,14 @@ class NamedCustomFunction(NamedArgument, ABC): class ParsedCustomFunction(NamedCustomFunction): num_input_args: int + def definition_name(self) -> str: + """ + Returns + ------- + The function definition name, including the % + """ + return f"%{self.name}" + @dataclass(frozen=True) class FunctionType(NamedArgument, ABC): diff --git a/src/ytdl_sub/script/types/variable_dependency.py b/src/ytdl_sub/script/types/variable_dependency.py index 18f2dab2..37130b7a 100644 --- a/src/ytdl_sub/script/types/variable_dependency.py +++ b/src/ytdl_sub/script/types/variable_dependency.py @@ -160,9 +160,20 @@ class VariableDependency(ABC): raise UNREACHABLE @final - def _custom_function_dependencies( + def custom_function_dependencies( self, custom_function_definitions: Dict[str, "VariableDependency"] ) -> Set[ParsedCustomFunction]: + """ + Parameters + ---------- + custom_function_definitions + Definition of all currently existing custom functions. Needed to check whether + a lambda function's input function is custom or not. + + Returns + ------- + All custom function dependencies + """ custom_functions = self.custom_functions for lambda_func in self.lambdas: if lambda_func.value in custom_function_definitions: @@ -185,7 +196,7 @@ class VariableDependency(ABC): True if it contains all input variables as a dependency. False otherwise. """ # If there are lambdas, see if they are custom functions. If so, check them - for custom_function in self._custom_function_dependencies(custom_function_definitions): + for custom_function in self.custom_function_dependencies(custom_function_definitions): if not custom_function_definitions[custom_function.name].is_subset_of( variables=variables, custom_function_definitions=custom_function_definitions ): @@ -205,7 +216,7 @@ class VariableDependency(ABC): True if it contains any of the input variables. False otherwise. """ # If there are lambdas, see if they are custom functions. If so, check them - for custom_function in self._custom_function_dependencies(custom_function_definitions): + for custom_function in self.custom_function_dependencies(custom_function_definitions): if custom_function_definitions[custom_function.name].contains( variables=variables, custom_function_definitions=custom_function_definitions ): diff --git a/tests/unit/script/types/test_lambda_function.py b/tests/unit/script/types/test_lambda_function.py index aac78101..cd054811 100644 --- a/tests/unit/script/types/test_lambda_function.py +++ b/tests/unit/script/types/test_lambda_function.py @@ -6,6 +6,7 @@ from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments @@ -169,3 +170,10 @@ class TestLambdaFunctionIncompatibleNumArguments: "%output": f"{{%array_enumerate(array1, {lambda_value})}}", } ) + + def test_lambda_with_custom_function_cycle(self): + with pytest.raises( + CycleDetected, + match=re.escape("Cycle detected within these variables: two -> %times_two -> two"), + ): + Script({"%times_two": "{%mul($0, two)}", "two": "{%times_two(2)}"})