diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index f1011fcf..623fc06d 100644 --- a/src/ytdl_sub/config/overrides.py +++ b/src/ytdl_sub/config/overrides.py @@ -3,8 +3,6 @@ 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 @@ -22,11 +20,10 @@ 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): """ @@ -210,8 +207,7 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): formatter: StringFormatterValidator, entry: Optional[Entry] = None, function_overrides: Optional[Dict[str, str]] = None, - expected_type: Type[ExpectedT] = str, - ) -> ExpectedT: + ) -> str: """ Parameters ---------- @@ -221,8 +217,6 @@ 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 ------- @@ -233,15 +227,42 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): StringFormattingException If the formatter that is trying to be resolved cannot """ - out = formatter.post_process( + 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( self._apply_to_resolvable( - formatter=formatter, entry=entry, function_overrides=function_overrides + formatter=formatter, entry=None, function_overrides=function_overrides ).native ) - if not isinstance(out, expected_type): - raise StringFormattingException( - f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'" - ) - - return out + 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) diff --git a/src/ytdl_sub/config/plugin/plugin.py b/src/ytdl_sub/config/plugin/plugin.py index 23232c48..0cd02b53 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.apply_formatter(self.plugin_options.enable, expected_type=bool) + return self.overrides.evaluate_boolean(self.plugin_options.enable) 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 db07ca02..93f36453 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -62,7 +62,10 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator): Materializes the entire ytdl-options dict from OverrideStringFormatters into native python. """ - out = {key: overrides.apply_formatter(val) for key, val in self.dict.items()} + out = { + key: overrides.apply_overrides_formatter_to_native(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 c94d6910..acb35895 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_formatter(validator.url, expected_type=list): + if entry_input_url in self.overrides.apply_overrides_formatter_to_native(validator.url): 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_formatter(validator.url, expected_type=list): + if entry_input_url in self.overrides.apply_overrides_formatter_to_native(validator.url): 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.apply_formatter(validator.download_reverse, expected_type=bool): + if self.overrides.evaluate_boolean(validator.download_reverse): 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.apply_formatter( - validator.include_sibling_metadata, expected_type=bool + include_sibling_metadata = self.overrides.evaluate_boolean( + validator.include_sibling_metadata ) parents, orphan_entries = self._download_url_metadata( @@ -487,9 +487,11 @@ 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, expected_type=list)): + if not (urls := self.overrides.apply_overrides_formatter_to_native(url_validator.url)): 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 b2010ebc..551d7169 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -1,6 +1,5 @@ from typing import Any from typing import Dict -from typing import List from typing import Optional from typing import Set @@ -45,7 +44,7 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]): class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator): - def post_process(self, resolved: Any) -> List[str]: + def post_process_native(self, resolved: Any) -> Any: 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 0cba6f89..478cf938 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.apply_formatter(self.plugin_options.breaks, expected_type=bool): + if self.overrides.evaluate_boolean(self.plugin_options.breaks): 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 4ce2b144..e7d19b8b 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.apply_formatter(self.plugin_options, expected_type=bool) + return self.overrides.evaluate_boolean(self.plugin_options) @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 d4adcc86..7d256998 100644 --- a/src/ytdl_sub/plugins/filter_exclude.py +++ b/src/ytdl_sub/plugins/filter_exclude.py @@ -7,14 +7,13 @@ 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 BooleanFormatterValidator -from ytdl_sub.validators.validators import ListValidator +from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-exclude") -class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): +class FilterExcludeOptions(ListFormatterValidator, 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. @@ -30,8 +29,6 @@ class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsVali { %contains( %lower(description), '#short' ) } """ - _inner_list_type = BooleanFormatterValidator - class FilterExcludePlugin(Plugin[FilterExcludeOptions]): plugin_options_type = FilterExcludeOptions @@ -55,9 +52,7 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = self.overrides.apply_formatter( - formatter=formatter, entry=entry, expected_type=bool - ) + should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry) if should_exclude: logger.info( diff --git a/src/ytdl_sub/plugins/filter_include.py b/src/ytdl_sub/plugins/filter_include.py index 5002dfdb..c2bc5217 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.validators.string_formatter_validators import BooleanFormatterValidator -from ytdl_sub.validators.validators import ListValidator +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 logger = Logger.get("filter-include") -class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): +class FilterIncludeOptions(ListFormatterValidator, 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,8 +38,6 @@ class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsVali } """ - _inner_list_type = BooleanFormatterValidator - class FilterIncludePlugin(Plugin[FilterIncludeOptions]): plugin_options_type = FilterIncludeOptions @@ -63,8 +61,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = self.overrides.apply_formatter( - formatter=formatter, entry=entry, expected_type=bool + should_exclude = ScriptUtils.bool_formatter_output( + self.overrides.apply_formatter(formatter=formatter, entry=entry) ) 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 802dab63..24c07c72 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.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool): + if self.overrides.evaluate_boolean(self.plugin_options.kodi_safe): 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 dc5db0b7..030f5a09 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.apply_formatter(self.plugin_options, expected_type=bool) + return self.overrides.evaluate_boolean(self.plugin_options) @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 463d1ace..7acad676 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 = overrides.apply_formatter(self._min, entry=entry, expected_type=float) - actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) + actualized_min = float(overrides.apply_formatter(self._min, entry=entry)) + actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) if actualized_min < 0: raise self._validation_exception( @@ -70,7 +70,7 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC): ------- Max possible value """ - actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) + actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) 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 3783293d..21715c03 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -82,27 +82,35 @@ class StringFormatterValidator(StringValidator): """ return self._parsed - def post_process(self, resolved: Any) -> Any: + def post_process(self, resolved: str) -> str: """ Returns ------- - Apply any post processing to the resolved value. Defaults to casting it to string. + Apply any post processing to the resolved value """ - return str(resolved) + return resolved + + def post_process_native(self, resolved: Any) -> Any: + """ + Returns + ------- + Apply any post processing to the resolved native value. + """ + return resolved class FloatFormatterValidator(StringFormatterValidator): _expected_value_type_name = "float" - def post_process(self, resolved: str) -> float: + def post_process(self, resolved: str) -> str: try: - out = float(resolved) + float(resolved) except Exception as exc: raise self._validation_exception( f"Expected a float, but received '{resolved}'" ) from exc - return out + return resolved class StandardizedDateValidator(StringFormatterValidator): @@ -119,13 +127,6 @@ 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): """ @@ -145,14 +146,15 @@ class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator): _expected_value_type_name = "integer" - def post_process(self, resolved: str) -> int: + def post_process(self, resolved: str) -> str: try: - out = int(resolved) + int(resolved) except Exception as exc: raise self._validation_exception( f"Expected an integer, but received '{resolved}'" ) from exc - return out + + return resolved class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator): @@ -161,14 +163,9 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF """ -class OverridesBooleanFormatterValidator( - BooleanFormatterValidator, OverridesStringFormatterValidator -): +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/config/test_subscription.py b/tests/unit/config/test_subscription.py index 930708ed..35942f8d 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -475,6 +475,19 @@ 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 0f7e9c5e..07e30006 100644 --- a/tests/unit/prebuilt_presets/test_tv_show_collection.py +++ b/tests/unit/prebuilt_presets/test_tv_show_collection.py @@ -57,13 +57,12 @@ class TestTvShowCollectionPreset: # is_bilateral if i == 0: - url = sub.overrides.apply_formatter( + url = sub.overrides.apply_overrides_formatter_to_native( 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}" @@ -82,13 +81,12 @@ class TestTvShowCollectionPreset: # not bilateral else: for j in range(2): - url = sub.overrides.apply_formatter( + url = sub.overrides.apply_overrides_formatter_to_native( 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