[FEATURE] Ability to nest presets in subscription files

This commit is contained in:
Jesse Bannon 2023-09-28 00:22:35 -07:00
parent 3e41e0f59e
commit 5078cad67c
4 changed files with 181 additions and 42 deletions

View file

@ -1,4 +1,5 @@
import copy import copy
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
@ -6,6 +7,7 @@ from typing import Optional
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import Preset
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
from ytdl_sub.subscriptions.subscription_validators import SubscriptionValidator
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.yaml import load_yaml from ytdl_sub.utils.yaml import load_yaml
from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import LiteralDictValidator
@ -121,39 +123,21 @@ class Subscription(SubscriptionDownload):
config = copy.deepcopy(config) config = copy.deepcopy(config)
config.presets.dict[FILE_PRESET_APPLY_KEY] = file_preset.dict config.presets.dict[FILE_PRESET_APPLY_KEY] = file_preset.dict
for subscription_key, subscription_object in subscription_dict.items(): subscriptions_dict: Dict[str, Any] = {
key: obj
for key, obj in subscription_dict.items()
if key not in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]
}
# Skip file preset or value subscriptions_dicts = SubscriptionValidator(
if subscription_key in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]: name="",
continue value=subscriptions_dict,
config=config,
# If the subscription obj is just a string, set it to the override variable presets=[FILE_PRESET_APPLY_KEY] if has_file_preset else [],
# defined in FILE_SUBSCRIPTION_VALUE_KEY subscription_value=file_subscription_value,
if isinstance(subscription_object, str) and file_subscription_value: ).subscription_dicts()
subscription_object = {"overrides": {file_subscription_value: subscription_object}}
elif isinstance(subscription_object, dict):
pass
elif isinstance(subscription_object, str) and not file_subscription_value:
raise ValidationException(
f"Subscription {subscription_key} is a string, but the subscription value "
f"is not set to an override variable"
)
else:
raise ValidationException(
f"Subscription {subscription_key} should be in the form of a preset"
)
# If it has file_preset, inject it as a parent preset
if has_file_preset:
parent_preset = subscription_object.get("preset", [])
# Preset can be a single string
if isinstance(parent_preset, str):
parent_preset = [parent_preset]
# If it's not a string or list, it will fail downstream
if isinstance(parent_preset, list):
subscription_object["preset"] = parent_preset + [FILE_PRESET_APPLY_KEY]
for subscription_key, subscription_object in subscriptions_dicts.items():
subscriptions.append( subscriptions.append(
cls.from_dict( cls.from_dict(
config=config, config=config,

View file

@ -0,0 +1,111 @@
import copy
from abc import ABC
from abc import abstractmethod
from typing import Dict
from typing import List
from typing import Optional
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import StringValidator
from ytdl_sub.validators.validators import Validator
class SubscriptionOutput(Validator, ABC):
def __init__(self, name, value, presets: List[str]):
super().__init__(name, value)
self._presets = copy.deepcopy(presets)
@abstractmethod
def subscription_dicts(self) -> Dict[str, Dict]:
"""
Returns
-------
Subscriptions in the form of ``{ subscription_name: preset_dict }``
"""
class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator):
def subscription_dicts(self) -> Dict[str, Dict]:
output_dict = copy.deepcopy(self._dict)
parent_presets = output_dict.get("preset", [])
# Preset can be a single string
if isinstance(parent_presets, str):
parent_presets = [parent_presets]
output_dict["preset"] = parent_presets + self._presets
return {self._leaf_name: output_dict}
class SubscriptionValueValidator(SubscriptionOutput, StringValidator):
def __init__(self, name, value, presets: List[str], subscription_value: Optional[str]):
super().__init__(name, value, presets)
if subscription_value is None:
raise self._validation_exception(
f"Subscription {name} is a string, but the subscription value "
f"is not set to an override variable"
)
self._subscription_value: str = subscription_value
def subscription_dicts(self) -> Dict[str, Dict]:
return {
self._leaf_name: {
"preset": self._presets,
"overrides": {self._subscription_value: self.value},
}
}
class SubscriptionValidator(SubscriptionOutput):
"""
Top-level subscription validator
"""
def __init__(
self, name, value, config: ConfigFile, presets: List[str], subscription_value: Optional[str]
):
super().__init__(name, value, presets)
self._children: List[SubscriptionOutput] = []
for key, obj in value.items():
obj_name = f"{name}.{key}" if name else key
if isinstance(obj, str):
if key in config.presets.keys:
raise self._validation_exception(
f"{key} conflicts with an existing preset name and cannot be "
f"used as a subscription name"
)
self._children.append(
SubscriptionValueValidator(
name=obj_name,
value=obj,
presets=presets,
subscription_value=subscription_value,
)
)
elif isinstance(value, dict):
if key in config.presets.keys:
self._children.append(
SubscriptionValidator(
name=obj_name,
value=obj,
config=config,
presets=presets + [key],
subscription_value=subscription_value,
)
)
else:
self._children.append(
SubscriptionPresetDictValidator(name=obj_name, value=obj, presets=presets)
)
def subscription_dicts(self) -> Dict[str, Dict]:
subscription_dicts: Dict[str, Dict] = {}
for child in self._children:
subscription_dicts = dict(subscription_dicts, **child.subscription_dicts())
return subscription_dicts

View file

@ -93,6 +93,26 @@ class Validator(ABC):
""" """
return validation_exception(self._name, error_message, exception_class) return validation_exception(self._name, error_message, exception_class)
@final
@property
def _root_name(self) -> str:
"""
Returns
-------
"first" from the first.element.of.the.name
"""
return self._name.split(".")[0]
@final
@property
def _leaf_name(self) -> str:
"""
Returns
-------
"first" from the first.element.of.the.name
"""
return self._name.split(".")[-1]
class ValueValidator(Validator, ABC, Generic[ValueT]): class ValueValidator(Validator, ABC, Generic[ValueT]):
""" """
@ -179,16 +199,6 @@ class DictValidator(Validator):
super().__init__(name, value) super().__init__(name, value)
self.__validator_dict: Dict[str, Validator] = {} self.__validator_dict: Dict[str, Validator] = {}
@final
@property
def _root_name(self) -> str:
"""
Returns
-------
"first" from the first.element.of.the.name
"""
return self._name.split(".")[0]
@final @final
@property @property
def _dict(self) -> dict: def _dict(self) -> dict:

View file

@ -74,6 +74,19 @@ def preset_with_subscription_file_value(preset_with_subscription_value: Dict):
}, },
) )
@pytest.fixture
def preset_with_subscription_file_value_nested_presets(preset_with_subscription_value: Dict):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2": {
"parent_preset_1": {
"test_2_1": "is_2_1_overwritten"
},
"test_1": "is_1_overwritten"
}
}
)
def test_subscription_file_preset_applies(config_file: ConfigFile, preset_with_file_preset: Dict): def test_subscription_file_preset_applies(config_file: ConfigFile, preset_with_file_preset: Dict):
with mock_load_yaml(preset_dict=preset_with_file_preset): with mock_load_yaml(preset_dict=preset_with_file_preset):
@ -160,6 +173,27 @@ def test_subscription_file_value_applies_from_config(
) )
def test_subscription_file_value_applies_from_config_and_nested(
config_file_with_subscription_value: ConfigFile, preset_with_subscription_file_value_nested_presets: Dict
):
with mock_load_yaml(preset_dict=preset_with_subscription_file_value_nested_presets):
subs = Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked"
)
assert len(subs) == 2
# Test __value__ worked correctly from the config
value_sub = subs[1]
assert value_sub.name == "test_value"
assert (
value_sub.overrides.dict_with_format_strings.get("test_file_subscription_value")
== "original"
)
assert (
value_sub.overrides.dict_with_format_strings.get("test_config_subscription_value")
== "is_overwritten"
)
def test_subscription_file_bad_value(config_file: ConfigFile): def test_subscription_file_bad_value(config_file: ConfigFile):
with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises( with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises(
ValidationException, ValidationException,
@ -187,6 +221,6 @@ def test_subscription_file_using_value_when_not_defined(config_file: ConfigFile)
def test_subscription_file_invalid_form(config_file: ConfigFile): def test_subscription_file_invalid_form(config_file: ConfigFile):
with mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises( with mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises(
ValidationException, ValidationException,
match=re.escape(f"Subscription sub_name should be in the form of a preset"), match=re.escape(f"Validation error in sub_name: should be of type object."),
): ):
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked") _ = Subscription.from_file_path(config=config_file, subscription_path="mocked")