[FEATURE] Ability to nest presets in subscription files
This commit is contained in:
parent
3e41e0f59e
commit
5078cad67c
4 changed files with 181 additions and 42 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
|
@ -6,6 +7,7 @@ from typing import Optional
|
|||
from ytdl_sub.config.config_file import ConfigFile
|
||||
from ytdl_sub.config.preset import Preset
|
||||
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.yaml import load_yaml
|
||||
from ytdl_sub.validators.validators import LiteralDictValidator
|
||||
|
|
@ -121,39 +123,21 @@ class Subscription(SubscriptionDownload):
|
|||
config = copy.deepcopy(config)
|
||||
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
|
||||
if subscription_key in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]:
|
||||
continue
|
||||
|
||||
# If the subscription obj is just a string, set it to the override variable
|
||||
# defined in FILE_SUBSCRIPTION_VALUE_KEY
|
||||
if isinstance(subscription_object, str) and file_subscription_value:
|
||||
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]
|
||||
subscriptions_dicts = SubscriptionValidator(
|
||||
name="",
|
||||
value=subscriptions_dict,
|
||||
config=config,
|
||||
presets=[FILE_PRESET_APPLY_KEY] if has_file_preset else [],
|
||||
subscription_value=file_subscription_value,
|
||||
).subscription_dicts()
|
||||
|
||||
for subscription_key, subscription_object in subscriptions_dicts.items():
|
||||
subscriptions.append(
|
||||
cls.from_dict(
|
||||
config=config,
|
||||
|
|
|
|||
111
src/ytdl_sub/subscriptions/subscription_validators.py
Normal file
111
src/ytdl_sub/subscriptions/subscription_validators.py
Normal 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
|
||||
|
|
@ -93,6 +93,26 @@ class Validator(ABC):
|
|||
"""
|
||||
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]):
|
||||
"""
|
||||
|
|
@ -179,16 +199,6 @@ class DictValidator(Validator):
|
|||
super().__init__(name, value)
|
||||
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
|
||||
@property
|
||||
def _dict(self) -> dict:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
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):
|
||||
with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises(
|
||||
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):
|
||||
with mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises(
|
||||
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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue