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 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

View file

@ -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]]:

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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(

View file

@ -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(

View file

@ -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): [

View file

@ -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:

View file

@ -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}"

View file

@ -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

View file

@ -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

View file

@ -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(
{