From a71577e703b44e3c53c4e32181e8b03ab878d8ba Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Sun, 23 Oct 2022 20:36:25 -0700 Subject: [PATCH] [BACKEND] Partially validate all presets in configs --- src/ytdl_sub/config/preset.py | 13 +++++ src/ytdl_sub/config/preset_options.py | 9 ++++ .../generic/collection_validator.py | 7 +++ src/ytdl_sub/downloaders/youtube/channel.py | 9 ++++ src/ytdl_sub/plugins/audio_extract.py | 10 +++- src/ytdl_sub/plugins/nfo_tags.py | 20 +++++--- src/ytdl_sub/validators/nfo_validators.py | 1 + .../validators/strict_dict_validator.py | 1 + src/ytdl_sub/validators/validators.py | 47 ++++++++++++++++++- tests/unit/config/test_preset.py | 4 ++ 10 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index 077443ee..04cd96ef 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -128,6 +128,19 @@ class Preset(StrictDictValidator): # and ensure required keys are present. _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 def _source_variables(self) -> List[str]: return Entry.source_variables() diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index 4ed67789..d09c50fa 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,4 +1,5 @@ from abc import ABC +from typing import Any from typing import Dict from typing import List 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 StringFormatterValidator from ytdl_sub.validators.validators import BoolValidator +from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import LiteralDictValidator @@ -182,6 +184,13 @@ class OutputOptions(StrictDictValidator): "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): super().__init__(name, value) diff --git a/src/ytdl_sub/downloaders/generic/collection_validator.py b/src/ytdl_sub/downloaders/generic/collection_validator.py index 5af9ebaf..c25ac56c 100644 --- a/src/ytdl_sub/downloaders/generic/collection_validator.py +++ b/src/ytdl_sub/downloaders/generic/collection_validator.py @@ -1,3 +1,4 @@ +from typing import Any from typing import Dict from typing import List from typing import Optional @@ -42,6 +43,12 @@ class CollectionUrlValidator(StrictDictValidator): _required_keys = {"url"} _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): super().__init__(name, value) diff --git a/src/ytdl_sub/downloaders/youtube/channel.py b/src/ytdl_sub/downloaders/youtube/channel.py index 166ed6e4..44b730a9 100644 --- a/src/ytdl_sub/downloaders/youtube/channel.py +++ b/src/ytdl_sub/downloaders/youtube/channel.py @@ -1,3 +1,4 @@ +from typing import Any from typing import Dict from typing import List from typing import Optional @@ -34,6 +35,14 @@ class YoutubeChannelDownloaderOptions(DownloaderValidator): "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): super().__init__(name, value) self._channel_url = self._validate_key( diff --git a/src/ytdl_sub/plugins/audio_extract.py b/src/ytdl_sub/plugins/audio_extract.py index 7703a786..5c04195f 100644 --- a/src/ytdl_sub/plugins/audio_extract.py +++ b/src/ytdl_sub/plugins/audio_extract.py @@ -1,4 +1,5 @@ import os.path +from typing import Any from typing import Dict from typing import Optional @@ -27,7 +28,14 @@ class AudioExtractOptions(PluginOptions): 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): super().__init__(name, value) diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index f44c0546..bcb5eb3f 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -2,6 +2,7 @@ import os from abc import ABC from collections import defaultdict from pathlib import Path +from typing import Any from typing import Dict from typing import List from typing import Optional @@ -28,16 +29,21 @@ class SharedNfoTagsOptions(PluginOptions): _required_keys = {"nfo_name", "nfo_root", "tags"} _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): super().__init__(name, value) - self._nfo_name = self._validate_key_if_present( - key="nfo_name", validator=StringFormatterValidator - ) - 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._nfo_name = self._validate_key(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._kodi_safe = self._validate_key_if_present( key="kodi_safe", validator=BoolValidator, default=False ).value diff --git a/src/ytdl_sub/validators/nfo_validators.py b/src/ytdl_sub/validators/nfo_validators.py index d5cdd819..8f1e9f38 100644 --- a/src/ytdl_sub/validators/nfo_validators.py +++ b/src/ytdl_sub/validators/nfo_validators.py @@ -1,5 +1,6 @@ from abc import ABC from collections import defaultdict +from typing import Any from typing import Dict from typing import List diff --git a/src/ytdl_sub/validators/strict_dict_validator.py b/src/ytdl_sub/validators/strict_dict_validator.py index e76f4eec..3e93a2cf 100644 --- a/src/ytdl_sub/validators/strict_dict_validator.py +++ b/src/ytdl_sub/validators/strict_dict_validator.py @@ -1,3 +1,4 @@ +from typing import Any from typing import List from typing import Set diff --git a/src/ytdl_sub/validators/validators.py b/src/ytdl_sub/validators/validators.py index e2235587..eb8a49b3 100644 --- a/src/ytdl_sub/validators/validators.py +++ b/src/ytdl_sub/validators/validators.py @@ -16,6 +16,29 @@ ValidationExceptionT = TypeVar("ValidationExceptionT", bound=ValidationException 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): """ 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 _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): self._name = name 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. """ - prefix = f"Validation error in {self._name}: " - return exception_class(f"{prefix}{error_message}") + return validation_exception(self._name, error_message, exception_class) class ValueValidator(Validator, ABC, Generic[ValueT]): @@ -231,6 +265,15 @@ class DictValidator(Validator): 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): """DictValidator with exposed dict and keys method""" diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py index c2d2efb9..b348acdf 100644 --- a/tests/unit/config/test_preset.py +++ b/tests/unit/config/test_preset.py @@ -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)