diff --git a/tests/unit/entries/test_entry.py b/tests/unit/entries/test_entry.py index 50a35b33..398ed173 100644 --- a/tests/unit/entries/test_entry.py +++ b/tests/unit/entries/test_entry.py @@ -8,6 +8,7 @@ from ytdl_subscribe.validators.base.string_formatter_validators import ( from ytdl_subscribe.validators.config.overrides.overrides_validator import ( OverridesValidator, ) +from ytdl_subscribe.validators.exceptions import ValidationException class TestEntry(object): @@ -68,6 +69,45 @@ class TestEntry(object): == expected_string ) + def test_entry_formatter_override_recursive(self, mock_entry): + overrides = OverridesValidator( + name="test", + value={ + "level_a": "level a", + "level_b": "level b and {level_a}", + "level_c": "level c and {level_b}", + }, + ) + + format_string = StringFormatterValidator( + name="test", value="level d and {level_c}" + ) + expected_string = "level d and level c and level b and level a" + + assert ( + mock_entry.apply_formatter(format_string, overrides=overrides) + == expected_string + ) + + def test_entry_formatter_override_recursive_fail_cycle(self, mock_entry): + overrides = OverridesValidator( + name="test", + value={ + "level_a": "{level_b}", + "level_b": "{level_a}", + }, + ) + + expected_error_msg = ( + f"Attempted to format 'test' but failed after reaching max recursion depth of 3. " + f"Try to keep variables dependent on only one other variable." + ) + + format_string = StringFormatterValidator(name="test", value="{level_a}") + + with pytest.raises(ValidationException, match=expected_error_msg): + _ = mock_entry.apply_formatter(format_string, overrides=overrides) + def test_entry_missing_kwarg(self, mock_entry): key = "dne" expected_error_msg = f"Expected '{key}' in Entry but does not exist." diff --git a/ytdl_subscribe/entries/entry.py b/ytdl_subscribe/entries/entry.py index e54c60ff..2d3b2ebf 100644 --- a/ytdl_subscribe/entries/entry.py +++ b/ytdl_subscribe/entries/entry.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from pathlib import Path from typing import Any from typing import Dict @@ -11,6 +12,7 @@ from ytdl_subscribe.validators.base.string_formatter_validators import ( from ytdl_subscribe.validators.config.overrides.overrides_validator import ( OverridesValidator, ) +from ytdl_subscribe.validators.exceptions import ValidationException class Entry: @@ -18,6 +20,8 @@ class Entry: Entry object to represent a single media object returned from yt-dlp. """ + _MAX_FORMATTER_RECURSION = 3 + def __init__(self, **kwargs): """ Initialize the entry using ytdl metadata @@ -129,4 +133,27 @@ class Entry: f"for {self.__class__.__name__}. Available fields: {available_fields}" ) - return formatter.format_string.format(**entry_dict) + format_string = formatter.format_string + variables_present = True + recursion_depth = 0 + while variables_present and recursion_depth < self._MAX_FORMATTER_RECURSION: + format_string = format_string.format(**OrderedDict(entry_dict)) + variables_present = ( + len( + StringFormatterValidator( + name="__recursive_formatter_update__", + value=format_string, + ).format_variables + ) + > 0 + ) + recursion_depth += 1 + + if variables_present: + raise ValidationException( + f"Attempted to format '{formatter._name}' but failed after reaching max recursion " + f"depth of {self._MAX_FORMATTER_RECURSION}. Try to keep variables dependent on " + f"only one other variable." + ) + + return format_string