diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index b5225ed2..f1011fcf 100644 --- a/src/ytdl_sub/config/overrides.py +++ b/src/ytdl_sub/config/overrides.py @@ -3,6 +3,8 @@ from typing import Dict from typing import Iterable from typing import Optional from typing import Set +from typing import Type +from typing import TypeVar from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.script.variable_definitions import VARIABLES @@ -23,6 +25,8 @@ from ytdl_sub.utils.scriptable import Scriptable from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator +ExpectedT = TypeVar("ExpectedT") + class Overrides(UnstructuredDictFormatterValidator, Scriptable): """ @@ -206,7 +210,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): formatter: StringFormatterValidator, entry: Optional[Entry] = None, function_overrides: Optional[Dict[str, str]] = None, - ) -> Any: + expected_type: Type[ExpectedT] = str, + ) -> ExpectedT: """ Parameters ---------- @@ -216,6 +221,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): Optional. Entry to add source variables to the formatter function_overrides Optional. Explicit values to override the overrides themselves and source variables + expected_type + The expected type that should return. Defaults to string. Returns ------- @@ -226,17 +233,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): StringFormattingException If the formatter that is trying to be resolved cannot """ - return formatter.post_process( + out = formatter.post_process( self._apply_to_resolvable( formatter=formatter, entry=entry, function_overrides=function_overrides ).native ) - def evaluate_boolean( - self, formatter: StringFormatterValidator, entry: Optional[Entry] = None - ) -> bool: - """ - Apply a formatter, and evaluate it to a boolean - """ - output = self.apply_formatter(formatter=formatter, entry=entry) - return ScriptUtils.bool_formatter_output(output) + if not isinstance(out, expected_type): + raise StringFormattingException( + f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'" + ) + + return out diff --git a/src/ytdl_sub/config/plugin/plugin.py b/src/ytdl_sub/config/plugin/plugin.py index 0cd02b53..23232c48 100644 --- a/src/ytdl_sub/config/plugin/plugin.py +++ b/src/ytdl_sub/config/plugin/plugin.py @@ -48,7 +48,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC): Returns True if enabled, False if disabled. """ if isinstance(self.plugin_options, ToggleableOptionsDictValidator): - return self.overrides.evaluate_boolean(self.plugin_options.enable) + return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool) return True def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]: diff --git a/src/ytdl_sub/downloaders/url/downloader.py b/src/ytdl_sub/downloaders/url/downloader.py index ad9fe272..e806519a 100644 --- a/src/ytdl_sub/downloaders/url/downloader.py +++ b/src/ytdl_sub/downloaders/url/downloader.py @@ -382,7 +382,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): entries_to_iter: List[Optional[Entry]] = entries indices = list(range(len(entries_to_iter))) - if self.overrides.evaluate_boolean(validator.download_reverse): + if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool): indices = reversed(indices) for idx in indices: @@ -461,8 +461,8 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides) ) - include_sibling_metadata = self.overrides.evaluate_boolean( - validator.include_sibling_metadata + include_sibling_metadata = self.overrides.apply_formatter( + validator.include_sibling_metadata, expected_type=bool ) parents, orphan_entries = self._download_url_metadata( @@ -487,11 +487,9 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): # download the bottom-most urls first since they are top-priority for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))): # URLs can be empty. If they are, then skip - if not (urls := self.overrides.apply_formatter(url_validator.url)): + if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)): continue - assert isinstance(urls, list) - for url in reversed(urls): assert isinstance(url, str) diff --git a/src/ytdl_sub/plugins/date_range.py b/src/ytdl_sub/plugins/date_range.py index 478cf938..0cba6f89 100644 --- a/src/ytdl_sub/plugins/date_range.py +++ b/src/ytdl_sub/plugins/date_range.py @@ -116,7 +116,7 @@ class DateRangePlugin(Plugin[DateRangeOptions]): date_validator=self.plugin_options.after, overrides=self.overrides ) after_filter = f"{date_type} >= {after_str}" - if self.overrides.evaluate_boolean(self.plugin_options.breaks): + if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool): breaking_match_filters.append(after_filter) else: match_filters.append(after_filter) diff --git a/src/ytdl_sub/plugins/embed_thumbnail.py b/src/ytdl_sub/plugins/embed_thumbnail.py index e7d19b8b..4ce2b144 100644 --- a/src/ytdl_sub/plugins/embed_thumbnail.py +++ b/src/ytdl_sub/plugins/embed_thumbnail.py @@ -33,7 +33,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]): @property def _embed_thumbnail(self) -> bool: - return self.overrides.evaluate_boolean(self.plugin_options) + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) @classmethod def _embed_video_thumbnail(cls, entry: Entry) -> None: diff --git a/src/ytdl_sub/plugins/filter_exclude.py b/src/ytdl_sub/plugins/filter_exclude.py index 7d256998..529c3ebd 100644 --- a/src/ytdl_sub/plugins/filter_exclude.py +++ b/src/ytdl_sub/plugins/filter_exclude.py @@ -52,7 +52,9 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool + ) if should_exclude: logger.info( diff --git a/src/ytdl_sub/plugins/filter_include.py b/src/ytdl_sub/plugins/filter_include.py index c2bc5217..caf090c1 100644 --- a/src/ytdl_sub/plugins/filter_include.py +++ b/src/ytdl_sub/plugins/filter_include.py @@ -7,7 +7,6 @@ from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -61,8 +60,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool ) if not should_exclude: logger.info( diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index 24c07c72..802dab63 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -140,7 +140,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC): if not nfo_tags: return - if self.overrides.evaluate_boolean(self.plugin_options.kodi_safe): + if self.overrides.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool): nfo_root = to_max_3_byte_utf8_string(nfo_root) nfo_tags = { to_max_3_byte_utf8_string(key): [ diff --git a/src/ytdl_sub/plugins/square_thumbnail.py b/src/ytdl_sub/plugins/square_thumbnail.py index 030f5a09..dc5db0b7 100644 --- a/src/ytdl_sub/plugins/square_thumbnail.py +++ b/src/ytdl_sub/plugins/square_thumbnail.py @@ -31,7 +31,7 @@ class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]): @property def _square_thumbnail(self) -> bool: - return self.overrides.evaluate_boolean(self.plugin_options) + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) @classmethod def _convert_to_square_thumbnail(cls, entry: Entry) -> None: diff --git a/src/ytdl_sub/plugins/throttle_protection.py b/src/ytdl_sub/plugins/throttle_protection.py index 7acad676..463d1ace 100644 --- a/src/ytdl_sub/plugins/throttle_protection.py +++ b/src/ytdl_sub/plugins/throttle_protection.py @@ -42,8 +42,8 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC): ) def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float: - actualized_min = float(overrides.apply_formatter(self._min, entry=entry)) - actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) + actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float) + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) if actualized_min < 0: raise self._validation_exception( @@ -70,7 +70,7 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC): ------- Max possible value """ - actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) if actualized_max < 0: raise self._validation_exception( f"max must be greater than zero, received {actualized_max}" diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index aad6a413..b3540731 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -94,15 +94,15 @@ class StringFormatterValidator(StringValidator): class FloatFormatterValidator(StringFormatterValidator): _expected_value_type_name = "float" - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: str) -> float: try: - float(resolved) + out = float(resolved) except Exception as exc: raise self._validation_exception( f"Expected a float, but received '{resolved}'" ) from exc - return resolved + return out class StandardizedDateValidator(StringFormatterValidator): @@ -138,15 +138,14 @@ class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator): _expected_value_type_name = "integer" - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: str) -> int: try: - int(resolved) + out = int(resolved) except Exception as exc: raise self._validation_exception( f"Expected an integer, but received '{resolved}'" ) from exc - - return resolved + return out class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator): @@ -158,6 +157,9 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator): _expected_value_type_name = "boolean" + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + class ListFormatterValidator(ListValidator[StringFormatterValidator]): _inner_list_type = StringFormatterValidator diff --git a/tests/unit/prebuilt_presets/test_tv_show_collection.py b/tests/unit/prebuilt_presets/test_tv_show_collection.py index b7f1a6e1..0f7e9c5e 100644 --- a/tests/unit/prebuilt_presets/test_tv_show_collection.py +++ b/tests/unit/prebuilt_presets/test_tv_show_collection.py @@ -63,6 +63,7 @@ class TestTvShowCollectionPreset: # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) assert url == [ f"youtube.com/playlist?url_{season_num}_{i}" @@ -87,6 +88,7 @@ class TestTvShowCollectionPreset: # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) # First instance is the first url to get thumbnails diff --git a/tests/unit/utils/test_script_utils.py b/tests/unit/utils/test_script_utils.py index 608066b5..cbf42790 100644 --- a/tests/unit/utils/test_script_utils.py +++ b/tests/unit/utils/test_script_utils.py @@ -42,25 +42,6 @@ class TestScriptUtils: output = single_variable_output(ScriptUtils.to_script(json_dict)) assert output == expected_output - @pytest.mark.parametrize( - "input_str, expected_output", - [ - ("", False), - ("true", True), - ("false", False), - ("[ ]", False), - ("{ }", False), - ("True", True), - ("False", False), - ("lol not False", True), - ("0", False), - ("-1", True), - ("1", True), - ], - ) - def test_bool_formatter_output(self, input_str: str, expected_output: bool): - assert ScriptUtils.bool_formatter_output(input_str) == expected_output - def test_to_syntax_tree(self): out = ScriptUtils.to_native_script( {