diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index 623fc06d..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 @@ -20,10 +22,11 @@ from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.scriptable import Scriptable -from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator 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): """ @@ -207,7 +210,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): formatter: StringFormatterValidator, entry: Optional[Entry] = None, function_overrides: Optional[Dict[str, str]] = None, - ) -> str: + expected_type: Type[ExpectedT] = str, + ) -> ExpectedT: """ Parameters ---------- @@ -217,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 ------- @@ -227,42 +233,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): StringFormattingException If the formatter that is trying to be resolved cannot """ - return formatter.post_process( - str( - self._apply_to_resolvable( - formatter=formatter, entry=entry, function_overrides=function_overrides - ) - ) - ) - - def apply_overrides_formatter_to_native( - self, - formatter: OverridesStringFormatterValidator, - function_overrides: Optional[Dict[str, str]] = None, - ) -> Any: - """ - Parameters - ---------- - formatter - Overrides formatter to apply - function_overrides - Optional. Explicit values to override the overrides themselves and source variables - - Returns - ------- - The native python form of the resolved variable - """ - return formatter.post_process_native( + out = formatter.post_process( self._apply_to_resolvable( - formatter=formatter, entry=None, function_overrides=function_overrides + 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/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index 93f36453..db07ca02 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -62,10 +62,7 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator): Materializes the entire ytdl-options dict from OverrideStringFormatters into native python. """ - out = { - key: overrides.apply_overrides_formatter_to_native(val) - for key, val in self.dict.items() - } + out = {key: overrides.apply_formatter(val) for key, val in self.dict.items()} if "cookiefile" in out: if not FileHandler.is_file_existent(out["cookiefile"]): raise ValidationException( diff --git a/src/ytdl_sub/downloaders/url/downloader.py b/src/ytdl_sub/downloaders/url/downloader.py index acb35895..c94d6910 100644 --- a/src/ytdl_sub/downloaders/url/downloader.py +++ b/src/ytdl_sub/downloaders/url/downloader.py @@ -52,12 +52,12 @@ class UrlDownloaderBasePluginExtension(SourcePluginExtension[MultiUrlValidator]) if 0 <= input_url_idx < len(self.plugin_options.urls.list): validator = self.plugin_options.urls.list[input_url_idx] - if entry_input_url in self.overrides.apply_overrides_formatter_to_native(validator.url): + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Match the first validator based on the URL, if one exists for validator in self.plugin_options.urls.list: - if entry_input_url in self.overrides.apply_overrides_formatter_to_native(validator.url): + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Return the first validator if none exist @@ -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_overrides_formatter_to_native(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/downloaders/url/validators.py b/src/ytdl_sub/downloaders/url/validators.py index 551d7169..b2010ebc 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -1,5 +1,6 @@ from typing import Any from typing import Dict +from typing import List from typing import Optional from typing import Set @@ -44,7 +45,7 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]): class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator): - def post_process_native(self, resolved: Any) -> Any: + def post_process(self, resolved: Any) -> List[str]: if isinstance(resolved, str): return [resolved] if isinstance(resolved, list): 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..d4adcc86 100644 --- a/src/ytdl_sub/plugins/filter_exclude.py +++ b/src/ytdl_sub/plugins/filter_exclude.py @@ -7,13 +7,14 @@ 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.validators.string_formatter_validators import ListFormatterValidator +from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-exclude") -class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): +class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional OR on any number of filters comprised of either variables or scripts. If any filter evaluates to True, the entry will be excluded. @@ -29,6 +30,8 @@ class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): { %contains( %lower(description), '#short' ) } """ + _inner_list_type = BooleanFormatterValidator + class FilterExcludePlugin(Plugin[FilterExcludeOptions]): plugin_options_type = FilterExcludeOptions @@ -52,7 +55,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..5002dfdb 100644 --- a/src/ytdl_sub/plugins/filter_include.py +++ b/src/ytdl_sub/plugins/filter_include.py @@ -7,14 +7,14 @@ 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.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-include") -class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): +class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional AND on any number of filters comprised of either variables or scripts. If all filters evaluate to True, the entry will be included. @@ -38,6 +38,8 @@ class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): } """ + _inner_list_type = BooleanFormatterValidator + class FilterIncludePlugin(Plugin[FilterIncludeOptions]): plugin_options_type = FilterIncludeOptions @@ -61,8 +63,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 21715c03..3783293d 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -82,35 +82,27 @@ class StringFormatterValidator(StringValidator): """ return self._parsed - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: Any) -> Any: """ Returns ------- - Apply any post processing to the resolved value + Apply any post processing to the resolved value. Defaults to casting it to string. """ - return resolved - - def post_process_native(self, resolved: Any) -> Any: - """ - Returns - ------- - Apply any post processing to the resolved native value. - """ - return resolved + return str(resolved) 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): @@ -127,6 +119,13 @@ class StandardizedDateValidator(StringFormatterValidator): return resolved +class BooleanFormatterValidator(StringFormatterValidator): + _expected_value_type_name = "boolean" + + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + + # pylint: disable=line-too-long class OverridesStringFormatterValidator(StringFormatterValidator): """ @@ -146,15 +145,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): @@ -163,9 +161,14 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF """ -class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator): +class OverridesBooleanFormatterValidator( + BooleanFormatterValidator, 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/config/test_subscription.py b/tests/unit/config/test_subscription.py index 35942f8d..930708ed 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -475,19 +475,6 @@ def test_advanced_tv_show_subscriptions( assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe" - assert overrides.apply_overrides_formatter_to_native(overrides.dict["subscription_array"]) == [ - "https://www.youtube.com/@gardeningwithciscoe4430", - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_", - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn", - "https://www.youtube.com/watch?v=2vq-vPubS5I", - ] - assert overrides.apply_overrides_formatter_to_native(overrides.dict["urls"]) == [ - "https://www.youtube.com/@gardeningwithciscoe4430", - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_", - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn", - "https://www.youtube.com/watch?v=2vq-vPubS5I", - ] - def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path): subs = Subscription.from_file_path( diff --git a/tests/unit/prebuilt_presets/test_tv_show_collection.py b/tests/unit/prebuilt_presets/test_tv_show_collection.py index 07e30006..0f7e9c5e 100644 --- a/tests/unit/prebuilt_presets/test_tv_show_collection.py +++ b/tests/unit/prebuilt_presets/test_tv_show_collection.py @@ -57,12 +57,13 @@ class TestTvShowCollectionPreset: # is_bilateral if i == 0: - url = sub.overrides.apply_overrides_formatter_to_native( + url = sub.overrides.apply_formatter( url_list[itr].url, function_overrides={ # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) assert url == [ f"youtube.com/playlist?url_{season_num}_{i}" @@ -81,12 +82,13 @@ class TestTvShowCollectionPreset: # not bilateral else: for j in range(2): - url = sub.overrides.apply_overrides_formatter_to_native( + url = sub.overrides.apply_formatter( url_list[itr + j].url, function_overrides={ # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) # First instance is the first url to get thumbnails