force expected type

This commit is contained in:
Jesse Bannon 2026-01-23 15:15:13 -08:00
parent 081d76c4e4
commit 5cb3028404
13 changed files with 43 additions and 54 deletions

View file

@ -3,6 +3,8 @@ 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
@ -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 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):
""" """
@ -206,7 +210,8 @@ 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,
) -> Any: expected_type: Type[ExpectedT] = str,
) -> ExpectedT:
""" """
Parameters Parameters
---------- ----------
@ -216,6 +221,8 @@ 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
------- -------
@ -226,17 +233,15 @@ 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
""" """
return formatter.post_process( out = formatter.post_process(
self._apply_to_resolvable( self._apply_to_resolvable(
formatter=formatter, entry=entry, function_overrides=function_overrides formatter=formatter, entry=entry, function_overrides=function_overrides
).native ).native
) )
def evaluate_boolean( if not isinstance(out, expected_type):
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None raise StringFormattingException(
) -> bool: f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'"
""" )
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.evaluate_boolean(self.plugin_options.enable) return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool)
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

@ -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.evaluate_boolean(validator.download_reverse): if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool):
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.evaluate_boolean( include_sibling_metadata = self.overrides.apply_formatter(
validator.include_sibling_metadata validator.include_sibling_metadata, expected_type=bool
) )
parents, orphan_entries = self._download_url_metadata( 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 # 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)): if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)):
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

@ -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.evaluate_boolean(self.plugin_options.breaks): if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool):
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.evaluate_boolean(self.plugin_options) return self.overrides.apply_formatter(self.plugin_options, expected_type=bool)
@classmethod @classmethod
def _embed_video_thumbnail(cls, entry: Entry) -> None: def _embed_video_thumbnail(cls, entry: Entry) -> None:

View file

@ -52,7 +52,9 @@ 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.evaluate_boolean(formatter=formatter, entry=entry) should_exclude = self.overrides.apply_formatter(
formatter=formatter, entry=entry, expected_type=bool
)
if should_exclude: if should_exclude:
logger.info( logger.info(

View file

@ -7,7 +7,6 @@ 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.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator 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
@ -61,8 +60,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 = ScriptUtils.bool_formatter_output( should_exclude = self.overrides.apply_formatter(
self.overrides.apply_formatter(formatter=formatter, entry=entry) formatter=formatter, entry=entry, expected_type=bool
) )
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.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_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.evaluate_boolean(self.plugin_options) return self.overrides.apply_formatter(self.plugin_options, expected_type=bool)
@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 = float(overrides.apply_formatter(self._min, entry=entry)) actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float)
actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float)
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 = float(overrides.apply_formatter(self._max, entry=entry)) actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float)
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

@ -94,15 +94,15 @@ class StringFormatterValidator(StringValidator):
class FloatFormatterValidator(StringFormatterValidator): class FloatFormatterValidator(StringFormatterValidator):
_expected_value_type_name = "float" _expected_value_type_name = "float"
def post_process(self, resolved: str) -> str: def post_process(self, resolved: str) -> float:
try: try:
float(resolved) out = 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 resolved return out
class StandardizedDateValidator(StringFormatterValidator): class StandardizedDateValidator(StringFormatterValidator):
@ -138,15 +138,14 @@ 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) -> str: def post_process(self, resolved: str) -> int:
try: try:
int(resolved) out = 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):
@ -158,6 +157,9 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF
class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator): class OverridesBooleanFormatterValidator(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

@ -63,6 +63,7 @@ class TestTvShowCollectionPreset:
# 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}"
@ -87,6 +88,7 @@ class TestTvShowCollectionPreset:
# 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

View file

@ -42,25 +42,6 @@ class TestScriptUtils:
output = single_variable_output(ScriptUtils.to_script(json_dict)) output = single_variable_output(ScriptUtils.to_script(json_dict))
assert output == expected_output 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): def test_to_syntax_tree(self):
out = ScriptUtils.to_native_script( out = ScriptUtils.to_native_script(
{ {