diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index fc7e9ac5..b1ba4347 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -38,6 +38,7 @@ from ytdl_sub.validators.string_formatter_validators import DictFormatterValidat from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatterValidator 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 validate_formatters from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import ListValidator from ytdl_sub.validators.validators import StringListValidator @@ -202,7 +203,7 @@ class Preset(_PresetShell): script = copy.deepcopy(self.overrides.script).add( ScriptUtils.add_dummy_variables(self._source_variables) ) - unresolved_variables = copy.deepcopy(self._added_variables) + unresolved_variables = self._added_variables added_variables: Set[str] = self.downloader_options.added_source_variables( unresolved_variables @@ -236,14 +237,14 @@ class Preset(_PresetShell): _ = script.resolve(unresolvable=unresolved_variables, update=True) # Validate that any formatter in the plugin options can resolve - self._validate_formatters( - mock_script=script, + validate_formatters( + script=script, unresolved_variables=unresolved_variables, validator=plugin_options, ) - self._validate_formatters( - mock_script=script, + validate_formatters( + script=script, unresolved_variables=unresolved_variables, validator=self.output_options, ) @@ -252,66 +253,6 @@ class Preset(_PresetShell): return script - def _validate_string_formatter_validator( - self, - mock_script: Script, - unresolved_variables: Set[str], - formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], - ) -> None: - try: - unresolvable = unresolved_variables - if isinstance(formatter_validator, OverridesStringFormatterValidator): - unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name}) - - mock_script.resolve_once( - {"tmp_var": formatter_validator.format_string}, - unresolvable=unresolvable, - ) - except VariableDoesNotExist as exc: - raise StringFormattingVariableNotFoundException(exc) from exc - - def _validate_formatters( - self, - mock_script: Script, - unresolved_variables: Set[str], - validator: Validator, - ) -> None: - """ - Ensure all OverridesStringFormatterValidator's only contain variables from the overrides - and resolve. - """ - if isinstance(validator, DictValidator): - # pylint: disable=protected-access - # Usage of protected variables in other validators is fine. The reason to keep - # them protected is for readability when using them in subscriptions. - for validator_value in validator._validator_dict.values(): - self._validate_formatters( - mock_script=mock_script, - unresolved_variables=unresolved_variables, - validator=validator_value, - ) - # pylint: enable=protected-access - elif isinstance(validator, ListValidator): - for list_value in validator.list: - self._validate_formatters( - mock_script=mock_script, - unresolved_variables=unresolved_variables, - validator=list_value, - ) - elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)): - self._validate_string_formatter_validator( - mock_script=mock_script, - unresolved_variables=unresolved_variables, - formatter_validator=validator, - ) - elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): - for validator_value in validator.dict.values(): - self._validate_string_formatter_validator( - mock_script=mock_script, - unresolved_variables=unresolved_variables, - formatter_validator=validator_value, - ) - def _get_presets_to_merge( self, parent_presets: str | List[str], seen_presets: List[str], config: ConfigValidator ) -> List[Dict]: @@ -349,7 +290,7 @@ class Preset(_PresetShell): return presets_to_merge - def __merge_parent_preset_dicts_if_present(self, config: ConfigValidator): + def _merge_parent_preset_dicts_if_present(self, config: ConfigValidator): parent_preset_validator = self._validate_key_if_present( key="preset", validator=StringListValidator ) @@ -372,7 +313,7 @@ class Preset(_PresetShell): super().__init__(name=name, value=value) # Perform the merge of parent presets before validating any keys - self.__merge_parent_preset_dicts_if_present(config=config) + self._merge_parent_preset_dicts_if_present(config=config) self.downloader_options: MultiUrlValidator = self._validate_key( key="download", validator=MultiUrlValidator diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index 635b79a8..4f408c4c 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -3,6 +3,8 @@ from collections import OrderedDict from keyword import iskeyword from typing import Dict from typing import List +from typing import Set +from typing import Union from typing import final from yt_dlp.utils import sanitize_filename @@ -12,9 +14,11 @@ from ytdl_sub.script.parser import parse from ytdl_sub.script.script import Script from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.utils.exceptions import UserException +from ytdl_sub.script.utils.exceptions import VariableDoesNotExist from ytdl_sub.utils.exceptions import InvalidVariableNameException from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException +from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import ListValidator from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import StringValidator @@ -172,3 +176,63 @@ class OverridesDictFormatterValidator(DictFormatterValidator): """ _key_validator = OverridesStringFormatterValidator + + +def _validate_formatter( + mock_script: Script, + unresolved_variables: Set[str], + formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], +) -> None: + try: + unresolvable = unresolved_variables + if isinstance(formatter_validator, OverridesStringFormatterValidator): + unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name}) + + mock_script.resolve_once( + {"tmp_var": formatter_validator.format_string}, + unresolvable=unresolvable, + ) + except VariableDoesNotExist as exc: + raise StringFormattingVariableNotFoundException(exc) from exc + + +def validate_formatters( + script: Script, + unresolved_variables: Set[str], + validator: Validator, +) -> None: + """ + Ensure all OverridesStringFormatterValidator's only contain variables from the overrides + and resolve. + """ + if isinstance(validator, DictValidator): + # pylint: disable=protected-access + # Usage of protected variables in other validators is fine. The reason to keep + # them protected is for readability when using them in subscriptions. + for validator_value in validator._validator_dict.values(): + validate_formatters( + script=script, + unresolved_variables=unresolved_variables, + validator=validator_value, + ) + # pylint: enable=protected-access + elif isinstance(validator, ListValidator): + for list_value in validator.list: + validate_formatters( + script=script, + unresolved_variables=unresolved_variables, + validator=list_value, + ) + elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)): + _validate_formatter( + mock_script=script, + unresolved_variables=unresolved_variables, + formatter_validator=validator, + ) + elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): + for validator_value in validator.dict.values(): + _validate_formatter( + mock_script=script, + unresolved_variables=unresolved_variables, + formatter_validator=validator_value, + ) diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py index 9e6cff71..f8a984fa 100644 --- a/tests/unit/config/test_preset.py +++ b/tests/unit/config/test_preset.py @@ -207,23 +207,3 @@ class TestPreset: "output_options": output_options, }, ) - - def test_preset_with_multi_url__contains_all_empty_urls_errors( - self, config_file, output_options - ): - with pytest.raises( - ValidationException, - match=re.escape( - "Validation error in test.download: Must contain at least one " - "url that is non-empty" - ), - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": [{"url": "{url}"}, {"url": "{url2}"}], - "output_options": output_options, - "overrides": {"url": "", "url2": ""}, - }, - )