[BACKEND] Partially validate all presets in configs

This commit is contained in:
Jesse Bannon 2022-10-23 20:36:25 -07:00
parent 3e4a995f6e
commit a71577e703
10 changed files with 111 additions and 10 deletions

View file

@ -128,6 +128,19 @@ class Preset(StrictDictValidator):
# and ensure required keys are present. # and ensure required keys are present.
_optional_keys = PRESET_KEYS _optional_keys = PRESET_KEYS
@classmethod
def preset_partial_validate(cls, config: ConfigFile, name: str, value: Any) -> None:
cls._partial_validate_key(name, value, "output_options", OutputOptions)
cls._partial_validate_key(name, value, "ytdl_options", YTDLOptions)
cls._partial_validate_key(name, value, "overrides", Overrides)
for plugin_name in PluginMapping.plugins():
cls._partial_validate_key(
name,
value,
key=plugin_name,
validator=PluginMapping.get(plugin_name).plugin_options_type,
)
@property @property
def _source_variables(self) -> List[str]: def _source_variables(self) -> List[str]:
return Entry.source_variables() return Entry.source_variables()

View file

@ -1,4 +1,5 @@
from abc import ABC from abc import ABC
from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -12,6 +13,7 @@ from ytdl_sub.validators.string_formatter_validators import DictFormatterValidat
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator 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 StringFormatterValidator
from ytdl_sub.validators.validators import BoolValidator from ytdl_sub.validators.validators import BoolValidator
from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import LiteralDictValidator
@ -182,6 +184,13 @@ class OutputOptions(StrictDictValidator):
"keep_files_after", "keep_files_after",
} }
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
if isinstance(value, dict):
value["output_directory"] = value.get("output_directory", "placeholder")
value["file_name"] = value.get("file_name", "placeholder")
_ = cls(name, value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)

View file

@ -1,3 +1,4 @@
from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -42,6 +43,12 @@ class CollectionUrlValidator(StrictDictValidator):
_required_keys = {"url"} _required_keys = {"url"}
_optional_keys = {"variables", "source_thumbnails", "playlist_thumbnails"} _optional_keys = {"variables", "source_thumbnails", "playlist_thumbnails"}
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
if isinstance(value, dict):
value["url"] = value.get("url", "placeholder")
_ = cls(name, value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)

View file

@ -1,3 +1,4 @@
from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -34,6 +35,14 @@ class YoutubeChannelDownloaderOptions(DownloaderValidator):
"channel_banner_path", "channel_banner_path",
} }
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
if isinstance(value, dict):
value["channel_url"] = value.get(
"channel_url", "https://www.youtube.com/c/ProjectZombie603"
)
_ = cls(name, value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self._channel_url = self._validate_key( self._channel_url = self._validate_key(

View file

@ -1,4 +1,5 @@
import os.path import os.path
from typing import Any
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
@ -27,7 +28,14 @@ class AudioExtractOptions(PluginOptions):
quality: 128 quality: 128
""" """
_optional_keys = {"codec", "quality"} _required_keys = {"codec"}
_optional_keys = {"quality"}
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
if isinstance(value, dict):
value["codec"] = value.get("codec", "mp3")
_ = cls(name, value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)

View file

@ -2,6 +2,7 @@ import os
from abc import ABC from abc import ABC
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -28,16 +29,21 @@ class SharedNfoTagsOptions(PluginOptions):
_required_keys = {"nfo_name", "nfo_root", "tags"} _required_keys = {"nfo_name", "nfo_root", "tags"}
_optional_keys = {"kodi_safe"} _optional_keys = {"kodi_safe"}
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
if isinstance(value, dict):
value["nfo_name"] = value.get("nfo_name", "placeholder")
value["nfo_root"] = value.get("nfo_root", "placeholder")
value["tags"] = value.get("tags", {})
_ = cls(name=name, value=value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self._nfo_name = self._validate_key_if_present( self._nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator)
key="nfo_name", validator=StringFormatterValidator self._nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator)
) self._tags = self._validate_key(key="tags", validator=NfoTagsValidator)
self._nfo_root = self._validate_key_if_present(
key="nfo_root", validator=StringFormatterValidator
)
self._tags = self._validate_key_if_present(key="tags", validator=NfoTagsValidator)
self._kodi_safe = self._validate_key_if_present( self._kodi_safe = self._validate_key_if_present(
key="kodi_safe", validator=BoolValidator, default=False key="kodi_safe", validator=BoolValidator, default=False
).value ).value

View file

@ -1,5 +1,6 @@
from abc import ABC from abc import ABC
from collections import defaultdict from collections import defaultdict
from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List

View file

@ -1,3 +1,4 @@
from typing import Any
from typing import List from typing import List
from typing import Set from typing import Set

View file

@ -16,6 +16,29 @@ ValidationExceptionT = TypeVar("ValidationExceptionT", bound=ValidationException
ValidatorT = TypeVar("ValidatorT", bound="Validator") ValidatorT = TypeVar("ValidatorT", bound="Validator")
def validation_exception(
name: str,
error_message: str | Exception,
exception_class: Type[ValidationExceptionT] = ValidationException,
) -> ValidationExceptionT:
"""
Parameters
----------
name
Name of the validator
error_message
Error message to include in the ValidationException
exception_class
Class of the exception
Returns
-------
Validation exception with a consistent prefix.
"""
prefix = f"Validation error in {name}: "
return exception_class(f"{prefix}{error_message}")
class Validator(ABC): class Validator(ABC):
""" """
Used to validate the value of a python object. This is the 'base' class that will first Used to validate the value of a python object. This is the 'base' class that will first
@ -29,6 +52,18 @@ class Validator(ABC):
# When raising an error, call the type this value instead of its python name # When raising an error, call the type this value instead of its python name
_expected_value_type_name: Optional[str] = None _expected_value_type_name: Optional[str] = None
@classmethod
def partial_validate(cls, name: str, value: Any) -> None:
"""
Parameters
----------
name
Name of the validator
value
Value of the validator
"""
_ = cls(name=name, value=value)
def __init__(self, name: str, value: Any): def __init__(self, name: str, value: Any):
self._name = name self._name = name
self._value = copy.deepcopy(value) # Always deep copy to avoid editing references self._value = copy.deepcopy(value) # Always deep copy to avoid editing references
@ -56,8 +91,7 @@ class Validator(ABC):
------- -------
Validation exception with a consistent prefix. Validation exception with a consistent prefix.
""" """
prefix = f"Validation error in {self._name}: " return validation_exception(self._name, error_message, exception_class)
return exception_class(f"{prefix}{error_message}")
class ValueValidator(Validator, ABC, Generic[ValueT]): class ValueValidator(Validator, ABC, Generic[ValueT]):
@ -231,6 +265,15 @@ class DictValidator(Validator):
return self._validate_key(key=key, validator=validator, default=default) return self._validate_key(key=key, validator=validator, default=default)
@final
@classmethod
def _partial_validate_key(
cls, name: str, value: Any, key: str, validator: Type[ValidatorT]
) -> None:
value_dict = DictValidator(name=name, value=value)
if key in value_dict._dict:
validator.partial_validate(name=f"{name}.{key}", value=value_dict._dict[key])
class LiteralDictValidator(DictValidator): class LiteralDictValidator(DictValidator):
"""DictValidator with exposed dict and keys method""" """DictValidator with exposed dict and keys method"""

View file

@ -202,3 +202,7 @@ class TestPreset:
}, },
}, },
) )
def test_partial_validate(self, config_file):
for preset_name, preset_dict in config_file.presets.dict.items():
Preset.preset_partial_validate(config_file, preset_name, preset_dict)