diff --git a/docs/source/config_reference/scripting/scripting_functions.rst b/docs/source/config_reference/scripting/scripting_functions.rst index c271669e..04dbbb49 100644 --- a/docs/source/config_reference/scripting/scripting_functions.rst +++ b/docs/source/config_reference/scripting/scripting_functions.rst @@ -38,10 +38,11 @@ array_apply_fixed array_at ~~~~~~~~ -:spec: ``array_at(array: Array, idx: Integer) -> AnyArgument`` +:spec: ``array_at(array: Array, idx: Integer, default: Optional[AnyArgument]) -> AnyArgument`` :description: - Return the element in the Array at index ``idx``. + Return the element in the Array at index ``idx``. If ``idx`` exceeds the array length, + either return ``default`` if provided or throw an error. array_contains ~~~~~~~~~~~~~~ @@ -225,6 +226,27 @@ xor Conditional Functions --------------------- +elif +~~~~ +:spec: ``elif(if_elif_else: AnyArgument, ...) -> AnyArgument`` + +:description: + Conditional ``if`` statement that is capable of doing else-ifs (``elif``) via + adjacent arguments. It is expected for there to be an odd number of arguments >= 3 to + supply at least one conditional and an else. +:usage: + + .. code-block:: python + + %elif( + condition1, + return1, + condition2, + return2, + ... + else_return + ) + if ~~ :spec: ``if(condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]`` diff --git a/src/ytdl_sub/script/functions/array_functions.py b/src/ytdl_sub/script/functions/array_functions.py index 4af6f63c..a07585e7 100644 --- a/src/ytdl_sub/script/functions/array_functions.py +++ b/src/ytdl_sub/script/functions/array_functions.py @@ -73,12 +73,18 @@ class ArrayFunctions: return Array(output) @staticmethod - def array_at(array: Array, idx: Integer) -> AnyArgument: + def array_at(array: Array, idx: Integer, default: Optional[AnyArgument] = None) -> AnyArgument: """ :description: - Return the element in the Array at index ``idx``. + Return the element in the Array at index ``idx``. If ``idx`` exceeds the array length, + either return ``default`` if provided or throw an error. """ - return array.value[idx.value] + try: + return array.value[idx.value] + except IndexError: + if default is not None: + return default + raise @staticmethod def array_first(array: Array, fallback: AnyArgument) -> AnyArgument: diff --git a/src/ytdl_sub/script/functions/conditional_functions.py b/src/ytdl_sub/script/functions/conditional_functions.py index e532031b..8b9da74b 100644 --- a/src/ytdl_sub/script/functions/conditional_functions.py +++ b/src/ytdl_sub/script/functions/conditional_functions.py @@ -1,8 +1,10 @@ from typing import Union +from ytdl_sub.script.types.resolvable import AnyArgument from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import ReturnableArgumentA from ytdl_sub.script.types.resolvable import ReturnableArgumentB +from ytdl_sub.script.utils.exceptions import FunctionRuntimeException class ConditionalFunctions: @@ -19,6 +21,39 @@ class ConditionalFunctions: return true return false + @staticmethod + def elif_(*if_elif_else: AnyArgument) -> AnyArgument: + """ + :description: + Conditional ``if`` statement that is capable of doing else-ifs (``elif``) via + adjacent arguments. It is expected for there to be an odd number of arguments >= 3 to + supply at least one conditional and an else. + :usage: + + .. code-block:: python + + %elif( + condition1, + return1, + condition2, + return2, + ... + else_return + ) + """ + arguments = list(if_elif_else) + if len(arguments) < 3: + raise FunctionRuntimeException("elif requires at least 3 arguments") + + if len(arguments) % 2 == 0: + raise FunctionRuntimeException("elif must have an odd number of arguments") + + for idx in range(0, len(arguments) - 1, 2): + if bool(arguments[idx].value): + return arguments[idx + 1] + + return arguments[-1] + @staticmethod def if_passthrough( maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB diff --git a/tests/unit/script/functions/test_array_functions.py b/tests/unit/script/functions/test_array_functions.py index 06a3b150..bfee05da 100644 --- a/tests/unit/script/functions/test_array_functions.py +++ b/tests/unit/script/functions/test_array_functions.py @@ -26,6 +26,14 @@ class TestArrayFunctions: output = single_variable_output("{%array_at(['a', 'b', 'c'], 1)}") assert output == "b" + def test_array_at_default(self): + output = single_variable_output("{%array_at(['a', 'b', 'c'], 30, 'd')}") + assert output == "d" + + def test_array_at_error(self): + with pytest.raises(FunctionRuntimeException): + single_variable_output("{%array_at(['a', 'b', 'c'], 30)}") + def test_array_flatten(self): output = single_variable_output("{%array_flatten(['a', ['b'], [['c']]])}") assert output == ["a", "b", "c"] diff --git a/tests/unit/script/functions/test_conditional_functions.py b/tests/unit/script/functions/test_conditional_functions.py index f7217915..5cf04a18 100644 --- a/tests/unit/script/functions/test_conditional_functions.py +++ b/tests/unit/script/functions/test_conditional_functions.py @@ -1,6 +1,10 @@ +import re + import pytest from unit.script.conftest import single_variable_output +from ytdl_sub.script.utils.exceptions import FunctionRuntimeException + class TestConditionalFunction: @pytest.mark.parametrize( @@ -33,3 +37,67 @@ class TestConditionalFunction: }""" ) assert output == "winner" + + def test_elif_function(self): + output = single_variable_output( + """{ + %elif( + False, + "nope", + False, + "still nope", + True, + "yes", + "default value" + ) + }""" + ) + assert output == "yes" + + def test_elif_function_default_value(self): + output = single_variable_output( + """{ + %elif( + False, + "nope", + False, + "still nope", + False, + "will be default", + "default value" + ) + }""" + ) + assert output == "default value" + + def test_elif_function_errors_lt3(self): + with pytest.raises( + FunctionRuntimeException, + match=re.escape("elif requires at least 3 arguments"), + ): + single_variable_output( + """ + { + %elif( + False, + "only two args" + ) + }""" + ) + + def test_elif_function_errors_odd(self): + with pytest.raises( + FunctionRuntimeException, + match=re.escape("elif must have an odd number of arguments"), + ): + single_variable_output( + """ + { + %elif( + False, + "1", + False, + "even number args bad" + ) + }""" + ) diff --git a/tests/unit/script/types/test_function.py b/tests/unit/script/types/test_function.py index 014e4a85..e13828f4 100644 --- a/tests/unit/script/types/test_function.py +++ b/tests/unit/script/types/test_function.py @@ -49,7 +49,11 @@ class TestFunction: @pytest.mark.parametrize( "function_str, expected_types, received_types", [ - ("{%array_at({'a': 'dict?'}, 1)}", "array: Array, idx: Integer", "Map, Integer"), + ( + "{%array_at({'a': 'dict?'}, 1)}", + "array: Array, idx: Integer, default: Optional[AnyArgument]", + "Map, Integer", + ), ("{%array_extend('not', 'array')}", "arrays: Array, ...", "String, String"), ( "{%replace('hi mom', 'mom', 'dad', 1, 0)}",