From 4447f456d516b7d08cf451df53838db555ae6585 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Tue, 28 Jun 2022 09:34:30 -0700 Subject: [PATCH] Validate all preset StringFormatValidators on init (#79) * fast fail * update error msg in test --- src/ytdl_sub/config/preset.py | 42 +++--- .../entries/variables/entry_variables.py | 16 +- .../validators/string_formatter_validators.py | 14 +- tests/unit/config/__init__.py | 0 tests/unit/config/test_preset.py | 141 ++++++++++++++++++ .../test_string_formatter_validator.py | 5 + 6 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 tests/unit/config/__init__.py create mode 100644 tests/unit/config/test_preset.py diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index 80c902e7..3e4e879e 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -5,6 +5,7 @@ from typing import List from typing import Optional from typing import Tuple from typing import Type +from typing import Union from mergedeep import mergedeep @@ -18,10 +19,12 @@ from ytdl_sub.downloaders.downloader import Downloader from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions -from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_sub.utils.yaml import load_yaml from ytdl_sub.validators.strict_dict_validator import StrictDictValidator +from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator +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.validators import DictValidator from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import Validator @@ -144,26 +147,22 @@ class Preset(StrictDictValidator): return plugins def __validate_override_string_formatter_validator( - self, formatter_validator: OverridesStringFormatterValidator + self, + formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], ): - # Gather all resolvable override variables - resolvable_override_variables: List[str] = [] - for name, override_variable in self.overrides.dict.items(): - try: - _ = override_variable.apply_formatter(self.overrides.dict_with_format_strings) - except StringFormattingVariableNotFoundException: - continue - resolvable_override_variables.append(name) + # Set the formatter variables to be the overrides + variable_dict = self.overrides.dict_with_format_strings - for variable_name in formatter_validator.format_variables: - if variable_name not in resolvable_override_variables: - # pylint: disable=protected-access - raise StringFormattingVariableNotFoundException( - f"Validation error in {formatter_validator._name}: " - f"This can only use override variables. Available override variables are: " - f"{', '.join(sorted(resolvable_override_variables))}", - ) - # pylint: enable=protected-access + # If the formatter supports source variables, set the formatter variables to include + # both source and override variables + if not isinstance(formatter_validator, OverridesStringFormatterValidator): + source_variables = { + source_var: "dummy_string" + for source_var in self.downloader.downloader_entry_type.source_variables() + } + variable_dict = dict(source_variables, **variable_dict) + + _ = formatter_validator.apply_formatter(variable_dict=variable_dict) def __recursive_preset_validate( self, @@ -184,8 +183,11 @@ class Preset(StrictDictValidator): self.__recursive_preset_validate(validator._validator_dict) # pylint: enable=protected-access - if isinstance(validator, OverridesStringFormatterValidator): + if isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)): self.__validate_override_string_formatter_validator(validator) + if isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): + for validator_value in validator.dict.values(): + self.__validate_override_string_formatter_validator(validator_value) def __merge_parent_preset_dicts_if_present(self, config: ConfigFile): parent_presets = set() diff --git a/src/ytdl_sub/entries/variables/entry_variables.py b/src/ytdl_sub/entries/variables/entry_variables.py index c2f19df4..46fa3766 100644 --- a/src/ytdl_sub/entries/variables/entry_variables.py +++ b/src/ytdl_sub/entries/variables/entry_variables.py @@ -1,4 +1,5 @@ from typing import Dict +from typing import List from typing import final from yt_dlp.utils import sanitize_filename @@ -36,6 +37,16 @@ class SourceVariables: """ return self.kwargs("extractor") + @classmethod + def source_variables(cls) -> List[str]: + """ + Returns + ------- + List of all source variables + """ + property_names = [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property)] + return property_names + @final def _to_dict(self) -> Dict[str, str]: """ @@ -43,10 +54,7 @@ class SourceVariables: ------- Dictionary containing all variables """ - cls = self.__class__ - property_names = [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property)] - - return {property_name: getattr(self, property_name) for property_name in property_names} + return {source_var: getattr(self, source_var) for source_var in self.source_variables()} class EntryVariables(SourceVariables): diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index b33db5b6..7b03dc17 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -39,6 +39,9 @@ class StringFormatterValidator(Validator): _expected_value_type = str _expected_value_type_name = "format string" + _variable_not_found_error_msg_formatter = ( + "Format variable '{variable_name}' does not exist. Available variables: {available_fields}" + ) __fields_validator = re.compile(r"{([a-z_]+?)}") @@ -103,14 +106,14 @@ class StringFormatterValidator(Validator): def __apply_formatter( self, formatter: "StringFormatterValidator", variable_dict: Dict[str, str] ) -> "StringFormatterValidator": - """TODO: test recursive case where variable not found""" # Ensure the variable names exist within the entry and overrides for variable_name in formatter.format_variables: if variable_name not in variable_dict: available_fields = ", ".join(sorted(variable_dict.keys())) raise self._validation_exception( - f"Format variable '{variable_name}' does not exist. " - f"Available variables: {available_fields}", + self._variable_not_found_error_msg_formatter.format( + variable_name=variable_name, available_fields=available_fields + ), exception_class=StringFormattingVariableNotFoundException, ) @@ -165,6 +168,11 @@ class OverridesStringFormatterValidator(StringFormatterValidator): :class:`nfo_output_directory ` """ + _variable_not_found_error_msg_formatter = ( + "Override variable '{variable_name}' does not exist. " + "Available override variables: {available_fields}" + ) + # pylint: enable=line-too-long diff --git a/tests/unit/config/__init__.py b/tests/unit/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py new file mode 100644 index 00000000..fa571631 --- /dev/null +++ b/tests/unit/config/test_preset.py @@ -0,0 +1,141 @@ +from typing import Dict + +import pytest + +from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.preset import Preset +from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException + + +@pytest.fixture +def config_file() -> ConfigFile: + return ConfigFile( + name="config", value={"configuration": {"working_directory": "."}, "presets": {}} + ) + + +@pytest.fixture +def output_options() -> Dict: + return { + "output_directory": "some dir", + "file_name": "{uid}", + } + + +class TestPreset: + @pytest.mark.parametrize( + "source, download_strategy", + [ + ("youtube", {"download_strategy": "video", "video_url": "youtube.com/watch?v=123abc"}), + ( + "youtube", + { + "download_strategy": "playlist", + "playlist_url": "youtube.com/playlist?list=123abc", + }, + ), + ("youtube", {"download_strategy": "channel", "channel_url": "youtube.com/c/123abc"}), + ( + "soundcloud", + {"download_strategy": "albums_and_singles", "url": "soundcloud.com/123abc"}, + ), + ], + ) + def test_bare_minimum_preset(self, config_file, output_options, source, download_strategy): + _ = Preset( + config=config_file, + name="test", + value={source: download_strategy, "output_options": output_options}, + ) + + def test_preset_with_override_variable(self, config_file, output_options): + _ = Preset( + config=config_file, + name="test", + value={ + "youtube": { + "download_strategy": "video", + "video_url": "youtube.com/watch?v=123abc", + }, + "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, + "overrides": {"dne_var": "not dne"}, + }, + ) + + def test_preset_error__source_variable_does_not_exist(self, config_file, output_options): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="Format variable 'dne_var' does not exist", + ): + _ = Preset( + config=config_file, + name="test", + value={ + "youtube": { + "download_strategy": "video", + "video_url": "youtube.com/watch?v=123abc", + }, + "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, + }, + ) + + def test_preset_error__override_variable_does_not_exist(self, config_file, output_options): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="Override variable 'dne_var' does not exist", + ): + _ = Preset( + config=config_file, + name="test", + value={ + "youtube": { + "download_strategy": "video", + "video_url": "youtube.com/watch?v=123abc", + }, + "output_options": {"output_directory": "{dne_var}", "file_name": "file"}, + }, + ) + + def test_preset_error__dict_source_variable_does_not_exist(self, config_file, output_options): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="Format variable 'dne_var' does not exist", + ): + _ = Preset( + config=config_file, + name="test", + value={ + "youtube": { + "download_strategy": "video", + "video_url": "youtube.com/watch?v=123abc", + }, + "output_options": {"output_directory": "dir", "file_name": "file"}, + "nfo_tags": { + "nfo_name": "the nfo name", + "nfo_root": "the root", + "tags": {"tag_a": "{dne_var}"}, + }, + }, + ) + + def test_preset_error__dict_override_variable_does_not_exist(self, config_file, output_options): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="Override variable 'dne_var' does not exist", + ): + _ = Preset( + config=config_file, + name="test", + value={ + "youtube": { + "download_strategy": "video", + "video_url": "youtube.com/watch?v=123abc", + }, + "output_options": {"output_directory": "dir", "file_name": "file"}, + "output_directory_nfo_tags": { + "nfo_name": "the nfo name", + "nfo_root": "the root", + "tags": {"tag_a": "{dne_var}"}, + }, + }, + ) diff --git a/tests/unit/validators/test_string_formatter_validator.py b/tests/unit/validators/test_string_formatter_validator.py index 5a94c1fc..d838da34 100644 --- a/tests/unit/validators/test_string_formatter_validator.py +++ b/tests/unit/validators/test_string_formatter_validator.py @@ -105,6 +105,11 @@ class TestStringFormatterValidator(object): f"Validation error in test: Format variable 'bah_humbug' does not exist. " f"Available variables: {', '.join(sorted(variable_dict.keys()))}" ) + if string_formatter_class == OverridesStringFormatterValidator: + expected_error_msg = ( + f"Validation error in test: Override variable 'bah_humbug' does not exist. " + f"Available override variables: {', '.join(sorted(variable_dict.keys()))}" + ) with pytest.raises(StringFormattingException, match=expected_error_msg): assert format_string.apply_formatter(variable_dict=variable_dict)