Revert "[BACKEND] More flexible formatter post_process (#1421)" (#1422)

This reverts commit 60cdbad8c7.

Caused a breaking change to ytdl_options
This commit is contained in:
Jesse Bannon 2026-01-24 00:22:18 -08:00 committed by GitHub
parent 60cdbad8c7
commit c1431c8d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 100 additions and 74 deletions

View file

@ -3,8 +3,6 @@ from typing import Dict
from typing import Iterable from typing import Iterable
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import Type
from typing import TypeVar
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES 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.exceptions import ValidationException
from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.utils.scriptable import Scriptable 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 StringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator
ExpectedT = TypeVar("ExpectedT")
class Overrides(UnstructuredDictFormatterValidator, Scriptable): class Overrides(UnstructuredDictFormatterValidator, Scriptable):
""" """
@ -210,8 +207,7 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
formatter: StringFormatterValidator, formatter: StringFormatterValidator,
entry: Optional[Entry] = None, entry: Optional[Entry] = None,
function_overrides: Optional[Dict[str, str]] = None, function_overrides: Optional[Dict[str, str]] = None,
expected_type: Type[ExpectedT] = str, ) -> str:
) -> ExpectedT:
""" """
Parameters Parameters
---------- ----------
@ -221,8 +217,6 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
Optional. Entry to add source variables to the formatter Optional. Entry to add source variables to the formatter
function_overrides function_overrides
Optional. Explicit values to override the overrides themselves and source variables Optional. Explicit values to override the overrides themselves and source variables
expected_type
The expected type that should return. Defaults to string.
Returns Returns
------- -------
@ -233,15 +227,42 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
StringFormattingException StringFormattingException
If the formatter that is trying to be resolved cannot 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( self._apply_to_resolvable(
formatter=formatter, entry=entry, function_overrides=function_overrides formatter=formatter, entry=None, function_overrides=function_overrides
).native ).native
) )
if not isinstance(out, expected_type): def evaluate_boolean(
raise StringFormattingException( self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'" ) -> bool:
) """
Apply a formatter, and evaluate it to a boolean
return out """
output = self.apply_formatter(formatter=formatter, entry=entry)
return ScriptUtils.bool_formatter_output(output)

View file

@ -48,7 +48,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
Returns True if enabled, False if disabled. Returns True if enabled, False if disabled.
""" """
if isinstance(self.plugin_options, ToggleableOptionsDictValidator): 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 return True
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]: def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:

View file

@ -62,7 +62,10 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
Materializes the entire ytdl-options dict from OverrideStringFormatters into Materializes the entire ytdl-options dict from OverrideStringFormatters into
native python. 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 "cookiefile" in out:
if not FileHandler.is_file_existent(out["cookiefile"]): if not FileHandler.is_file_existent(out["cookiefile"]):
raise ValidationException( raise ValidationException(

View file

@ -52,12 +52,12 @@ class UrlDownloaderBasePluginExtension(SourcePluginExtension[MultiUrlValidator])
if 0 <= input_url_idx < len(self.plugin_options.urls.list): if 0 <= input_url_idx < len(self.plugin_options.urls.list):
validator = self.plugin_options.urls.list[input_url_idx] 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 return validator
# Match the first validator based on the URL, if one exists # Match the first validator based on the URL, if one exists
for validator in self.plugin_options.urls.list: 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 validator
# Return the first validator if none exist # Return the first validator if none exist
@ -382,7 +382,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
entries_to_iter: List[Optional[Entry]] = entries entries_to_iter: List[Optional[Entry]] = entries
indices = list(range(len(entries_to_iter))) 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) indices = reversed(indices)
for idx in indices: for idx in indices:
@ -461,8 +461,8 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides) ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
) )
include_sibling_metadata = self.overrides.apply_formatter( include_sibling_metadata = self.overrides.evaluate_boolean(
validator.include_sibling_metadata, expected_type=bool validator.include_sibling_metadata
) )
parents, orphan_entries = self._download_url_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 # download the bottom-most urls first since they are top-priority
for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))): for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))):
# URLs can be empty. If they are, then skip # 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 continue
assert isinstance(urls, list)
for url in reversed(urls): for url in reversed(urls):
assert isinstance(url, str) assert isinstance(url, str)

View file

@ -1,6 +1,5 @@
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from typing import Set from typing import Set
@ -45,7 +44,7 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]):
class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator): class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator):
def post_process(self, resolved: Any) -> List[str]: def post_process_native(self, resolved: Any) -> Any:
if isinstance(resolved, str): if isinstance(resolved, str):
return [resolved] return [resolved]
if isinstance(resolved, list): if isinstance(resolved, list):

View file

@ -116,7 +116,7 @@ class DateRangePlugin(Plugin[DateRangeOptions]):
date_validator=self.plugin_options.after, overrides=self.overrides date_validator=self.plugin_options.after, overrides=self.overrides
) )
after_filter = f"{date_type} >= {after_str}" 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) breaking_match_filters.append(after_filter)
else: else:
match_filters.append(after_filter) match_filters.append(after_filter)

View file

@ -33,7 +33,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]):
@property @property
def _embed_thumbnail(self) -> bool: 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 @classmethod
def _embed_video_thumbnail(cls, entry: Entry) -> None: def _embed_video_thumbnail(cls, entry: Entry) -> None:

View file

@ -7,14 +7,13 @@ from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
from ytdl_sub.validators.validators import ListValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
logger = Logger.get("filter-exclude") 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. 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. If any filter evaluates to True, the entry will be excluded.
@ -30,8 +29,6 @@ class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsVali
{ %contains( %lower(description), '#short' ) } { %contains( %lower(description), '#short' ) }
""" """
_inner_list_type = BooleanFormatterValidator
class FilterExcludePlugin(Plugin[FilterExcludeOptions]): class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
plugin_options_type = FilterExcludeOptions plugin_options_type = FilterExcludeOptions
@ -55,9 +52,7 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
return entry return entry
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
should_exclude = self.overrides.apply_formatter( should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry)
formatter=formatter, entry=entry, expected_type=bool
)
if should_exclude: if should_exclude:
logger.info( logger.info(

View file

@ -7,14 +7,14 @@ from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator from ytdl_sub.utils.script import ScriptUtils
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 from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
logger = Logger.get("filter-include") 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. 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. 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]): class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
plugin_options_type = FilterIncludeOptions plugin_options_type = FilterIncludeOptions
@ -63,8 +61,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
return entry return entry
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
should_exclude = self.overrides.apply_formatter( should_exclude = ScriptUtils.bool_formatter_output(
formatter=formatter, entry=entry, expected_type=bool self.overrides.apply_formatter(formatter=formatter, entry=entry)
) )
if not should_exclude: if not should_exclude:
logger.info( logger.info(

View file

@ -140,7 +140,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
if not nfo_tags: if not nfo_tags:
return 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_root = to_max_3_byte_utf8_string(nfo_root)
nfo_tags = { nfo_tags = {
to_max_3_byte_utf8_string(key): [ to_max_3_byte_utf8_string(key): [

View file

@ -31,7 +31,7 @@ class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]):
@property @property
def _square_thumbnail(self) -> bool: 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 @classmethod
def _convert_to_square_thumbnail(cls, entry: Entry) -> None: def _convert_to_square_thumbnail(cls, entry: Entry) -> None:

View file

@ -42,8 +42,8 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC):
) )
def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float: def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float:
actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float) actualized_min = float(overrides.apply_formatter(self._min, entry=entry))
actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) actualized_max = float(overrides.apply_formatter(self._max, entry=entry))
if actualized_min < 0: if actualized_min < 0:
raise self._validation_exception( raise self._validation_exception(
@ -70,7 +70,7 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC):
------- -------
Max possible value 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: if actualized_max < 0:
raise self._validation_exception( raise self._validation_exception(
f"max must be greater than zero, received {actualized_max}" f"max must be greater than zero, received {actualized_max}"

View file

@ -82,27 +82,35 @@ class StringFormatterValidator(StringValidator):
""" """
return self._parsed return self._parsed
def post_process(self, resolved: Any) -> Any: def post_process(self, resolved: str) -> str:
""" """
Returns 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): class FloatFormatterValidator(StringFormatterValidator):
_expected_value_type_name = "float" _expected_value_type_name = "float"
def post_process(self, resolved: str) -> float: def post_process(self, resolved: str) -> str:
try: try:
out = float(resolved) float(resolved)
except Exception as exc: except Exception as exc:
raise self._validation_exception( raise self._validation_exception(
f"Expected a float, but received '{resolved}'" f"Expected a float, but received '{resolved}'"
) from exc ) from exc
return out return resolved
class StandardizedDateValidator(StringFormatterValidator): class StandardizedDateValidator(StringFormatterValidator):
@ -119,13 +127,6 @@ class StandardizedDateValidator(StringFormatterValidator):
return resolved 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 # pylint: disable=line-too-long
class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesStringFormatterValidator(StringFormatterValidator):
""" """
@ -145,14 +146,15 @@ class OverridesStringFormatterValidator(StringFormatterValidator):
class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator):
_expected_value_type_name = "integer" _expected_value_type_name = "integer"
def post_process(self, resolved: str) -> int: def post_process(self, resolved: str) -> str:
try: try:
out = int(resolved) int(resolved)
except Exception as exc: except Exception as exc:
raise self._validation_exception( raise self._validation_exception(
f"Expected an integer, but received '{resolved}'" f"Expected an integer, but received '{resolved}'"
) from exc ) from exc
return out
return resolved
class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator): class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator):
@ -161,14 +163,9 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF
""" """
class OverridesBooleanFormatterValidator( class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator):
BooleanFormatterValidator, OverridesStringFormatterValidator
):
_expected_value_type_name = "boolean" _expected_value_type_name = "boolean"
def post_process(self, resolved: Any) -> bool:
return ScriptUtils.bool_formatter_output(output=str(resolved))
class ListFormatterValidator(ListValidator[StringFormatterValidator]): class ListFormatterValidator(ListValidator[StringFormatterValidator]):
_inner_list_type = StringFormatterValidator _inner_list_type = StringFormatterValidator

View file

@ -475,6 +475,19 @@ def test_advanced_tv_show_subscriptions(
assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe" 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): def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path):
subs = Subscription.from_file_path( subs = Subscription.from_file_path(

View file

@ -57,13 +57,12 @@ class TestTvShowCollectionPreset:
# is_bilateral # is_bilateral
if i == 0: if i == 0:
url = sub.overrides.apply_formatter( url = sub.overrides.apply_overrides_formatter_to_native(
url_list[itr].url, url_list[itr].url,
function_overrides={ function_overrides={
# mock so bilateral url gets enabled # mock so bilateral url gets enabled
"subscription_has_download_archive": "True" "subscription_has_download_archive": "True"
}, },
expected_type=list,
) )
assert url == [ assert url == [
f"youtube.com/playlist?url_{season_num}_{i}" f"youtube.com/playlist?url_{season_num}_{i}"
@ -82,13 +81,12 @@ class TestTvShowCollectionPreset:
# not bilateral # not bilateral
else: else:
for j in range(2): for j in range(2):
url = sub.overrides.apply_formatter( url = sub.overrides.apply_overrides_formatter_to_native(
url_list[itr + j].url, url_list[itr + j].url,
function_overrides={ function_overrides={
# mock so bilateral url gets enabled # mock so bilateral url gets enabled
"subscription_has_download_archive": "True" "subscription_has_download_archive": "True"
}, },
expected_type=list,
) )
# First instance is the first url to get thumbnails # First instance is the first url to get thumbnails