Validate all preset StringFormatValidators on init (#79)

* fast fail

* update error msg in test
This commit is contained in:
Jesse Bannon 2022-06-28 09:34:30 -07:00 committed by GitHub
parent cb6dc8e034
commit 4447f456d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 191 additions and 27 deletions

View file

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

View file

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

View file

@ -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 <ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions>`
"""
_variable_not_found_error_msg_formatter = (
"Override variable '{variable_name}' does not exist. "
"Available override variables: {available_fields}"
)
# pylint: enable=line-too-long

View file

View file

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

View file

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