refactored, tests passing

This commit is contained in:
Jesse Bannon 2023-11-01 15:50:04 -07:00
parent 056b111be6
commit f2d60b3ade
4 changed files with 242 additions and 90 deletions

View file

@ -13,12 +13,12 @@ TV Show Full Archive:
"Opeth": "https://www.youtube.com/channel/UCmQSJTFZaXN85gYk6W3XbdQ"
# Sets "Kids" for genre, "TV-Y" for content rating
= Kids | TV-Y:
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
# All subscriptions under this will use the `TV Show Only Recent` preset
TV Show Only Recent:
= News | TV-14:
= News | = TV-14:
"BBC": "https://www.youtube.com/@BBCNews"

View file

@ -120,45 +120,9 @@ class YTDLOptions(LiteralDictValidator):
"""
class OverridesVariables(DictFormatterValidator):
"""
Override variables that are automatically added to every subscription.
"""
def _add_override_variable(self, key_name: str, format_string: str, sanitize: bool = False):
if sanitize:
key_name = f"{key_name}_sanitized"
format_string = sanitize_filename(format_string)
self._value[key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=format_string,
)
def __init__(self, name, value):
super().__init__(name, value)
# Add sanitized and non-sanitized override variables
for sanitize in [True, False]:
self._add_override_variable(
key_name="subscription_name",
format_string=self.subscription_name,
sanitize=sanitize,
)
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
return self._root_name
# Disable for proper docstring formatting
# pylint: disable=line-too-long
class Overrides(OverridesVariables):
class Overrides(DictFormatterValidator):
"""
Optional. This section allows you to define variables that can be used in any string formatter.
For example, if you want your file and thumbnail files to match without copy-pasting a large
@ -188,6 +152,16 @@ class Overrides(OverridesVariables):
# pylint: enable=line-too-long
def _add_override_variable(self, key_name: str, format_string: str, sanitize: bool = False):
if sanitize:
key_name = f"{key_name}_sanitized"
format_string = sanitize_filename(format_string)
self._value[key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=format_string,
)
def __init__(self, name, value):
super().__init__(name, value)

View file

@ -1,12 +1,15 @@
import copy
import dataclasses
from abc import ABC
from abc import abstractmethod
from typing import Dict
from typing import List
from typing import Optional
from typing import final
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.preset_options import Overrides
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import StringListValidator
from ytdl_sub.validators.validators import StringValidator
@ -36,18 +39,6 @@ def subscription_value_variable_name() -> str:
return "subscription_value"
def maybe_indent_override_values(value: str) -> List[str]:
"""
Returns
-------
Value if it is an overide [Value]. None otherwise.
"""
if value.startswith("="):
# Drop the =, split on |, and strip each indent_value (both left + right)
return [indent_value.strip() for indent_value in value[1:].split("|")]
return []
class SubscriptionOutput(Validator, ABC):
def __init__(self, name, value, presets: List[str], indent_overrides: List[str]):
super().__init__(name, value)
@ -75,6 +66,15 @@ class SubscriptionOutput(Validator, ABC):
Subscriptions in the form of ``{ subscription_name: preset_dict }``
"""
@property
def subscription_name(self) -> str:
"""
Returns
-------
The name of the subscription
"""
return self._leaf_name
class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator):
def __init__(self, name, value, presets: List[str], indent_overrides: List[str]):
@ -93,12 +93,46 @@ class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator):
output_dict["preset"] = parent_presets + self._presets + global_presets_to_apply
output_dict["overrides"] = dict(
output_dict.get("overrides", {}), **self._indent_overrides_dict()
output_dict.get("overrides", {}),
**self._indent_overrides_dict(),
**{"subscription_name": self.subscription_name},
)
return {self._leaf_name: output_dict}
return {self.subscription_name: output_dict}
class SubscriptionValueValidator(SubscriptionOutput, StringValidator):
class SubscriptionLeafValidator(SubscriptionOutput, ABC):
def __init__(
self,
name,
value,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(name=name, value=value, presets=presets, indent_overrides=indent_overrides)
if self.subscription_name in config.presets.keys:
raise self._validation_exception(
f"{self.subscription_name} conflicts with an existing preset name and cannot be "
f"used as a subscription name"
)
self._overrides_to_add: Dict[str, str] = {"subscription_name": self.subscription_name}
@final
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
return {
self.subscription_name: {
"preset": self._presets + global_presets_to_apply,
"overrides": dict(
self._indent_overrides_dict(),
**self._overrides_to_add,
),
}
}
class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
def __init__(
self,
name,
@ -108,30 +142,72 @@ class SubscriptionValueValidator(SubscriptionOutput, StringValidator):
indent_overrides: List[str],
subscription_value: Optional[str],
):
super().__init__(name=name, value=value, presets=presets, indent_overrides=indent_overrides)
super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
if self._leaf_name in config.presets.keys:
raise self._validation_exception(
f"{self._leaf_name} conflicts with an existing preset name and cannot be "
f"used as a subscription name"
)
self._subscription_value: Optional[str] = subscription_value
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
subscription_value_dict: Dict[str, str] = {"subscription_value": self.value}
# TODO: Eventually delete in favor of {subscription_value}
if self._subscription_value:
subscription_value_dict[self._subscription_value] = self.value
if subscription_value:
self._overrides_to_add[subscription_value] = self.value
self._overrides_to_add["subscription_value"] = self.value
return {
self._leaf_name: {
"preset": self._presets + global_presets_to_apply,
"overrides": dict(
subscription_value_dict,
**self._indent_overrides_dict(),
),
}
}
class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
def __init__(
self,
name,
value,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
for idx, list_value in enumerate(self.list):
# Write the first list value into subscription_value as well
if idx == 0:
self._overrides_to_add["subscription_value"] = list_value.value
self._overrides_to_add[f"subscription_value_{idx + 1}"] = list_value.value
class SubscriptionWithOverridesValidator(SubscriptionLeafValidator, DictFormatterValidator):
def __init__(
self,
name,
value,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
self._overrides_to_add = dict(self.dict_with_format_strings, **self._overrides_to_add)
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
# drop the ~ in "~Subscription Name":
return super().subscription_name[1:]
class SubscriptionValidator(SubscriptionOutput):
@ -139,6 +215,35 @@ class SubscriptionValidator(SubscriptionOutput):
Top-level subscription validator
"""
@dataclasses.dataclass
class PresetIndentKey:
presets: List[str] = dataclasses.field(default_factory=list)
indent_overrides: List[str] = dataclasses.field(default_factory=list)
def _preset_indent_key(self, key: str, config: ConfigFile) -> Optional[PresetIndentKey]:
presets: List[str] = []
indent_overrides: List[str] = []
stripped_split_keys = [sub_key.strip() for sub_key in key.split("|")]
for sub_key in stripped_split_keys:
if sub_key.startswith("="):
indent_overrides.append(sub_key[1:].strip())
elif sub_key in config.presets.keys:
presets.append(sub_key)
else:
if presets or indent_overrides:
raise self._validation_exception(
f"'{sub_key.strip()}' in '{key.strip()}' is not a preset name. "
f"To use as a subscription indent value, define it as '= {sub_key.strip()}'"
)
if not presets and not indent_overrides:
return None
return SubscriptionValidator.PresetIndentKey(
presets=presets, indent_overrides=indent_overrides
)
def __init__(
self,
name,
@ -154,6 +259,8 @@ class SubscriptionValidator(SubscriptionOutput):
for key, obj in value.items():
obj_name = f"{name}.{key}" if name else key
# Subscription defined as
# "Sub Name": "value"
if isinstance(obj, str):
self._children.append(
SubscriptionValueValidator(
@ -165,26 +272,45 @@ class SubscriptionValidator(SubscriptionOutput):
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],
indent_overrides=indent_overrides,
subscription_value=subscription_value,
)
# Subscription defined as
# "Sub Name":
# - "value1"
# - "value2"
elif isinstance(obj, list):
self._children.append(
SubscriptionListValuesValidator(
name=obj_name,
value=obj,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
elif override_values := maybe_indent_override_values(key):
)
elif isinstance(obj, dict):
# Subscription defined as
# "~Sub Name":
# override_1: "abc"
# override_2: "123"
if key.startswith("~"):
self._children.append(
SubscriptionValidator(
SubscriptionWithOverridesValidator(
name=obj_name,
value=obj,
config=config,
presets=presets,
indent_overrides=indent_overrides + override_values,
indent_overrides=indent_overrides,
)
)
elif (
preset_indent_key := self._preset_indent_key(key=key, config=config)
) is not None:
self._children.append(
SubscriptionValidator(
name=obj_name,
value=obj,
config=config,
presets=presets + preset_indent_key.presets,
indent_overrides=indent_overrides + preset_indent_key.indent_overrides,
subscription_value=subscription_value,
)
)
@ -197,6 +323,10 @@ class SubscriptionValidator(SubscriptionOutput):
indent_overrides=indent_overrides,
)
)
else:
raise self._validation_exception(
"Subscription value should either be a string, list, or object"
)
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
subscription_dicts: Dict[str, Dict] = {}

View file

@ -116,6 +116,25 @@ def preset_with_subscription_value_nested_presets_and_indent_variables(
@pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2": {
"=INDENT_1": {
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
"= INDENT_2 | = INDENT_3 ": {
"test_1": "is_1_overwritten",
},
}
}
},
)
@pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
@ -191,6 +210,7 @@ def test_subscription_file_value_applies_sub_file_takes_precedence(
assert value_sub.get("test_file_subscription_value") == "is_overwritten"
assert value_sub.get("test_config_subscription_value") == "original"
assert value_sub.get("subscription_name") == "test_value"
assert value_sub.get("subscription_name_sanitized") == "test_value"
assert value_sub.get("subscription_value") == "is_overwritten"
assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@ -209,6 +229,7 @@ def test_subscription_file_value_applies_from_config(
assert value_sub.get("test_file_subscription_value") == "original"
assert value_sub.get("test_config_subscription_value") == "is_overwritten"
assert value_sub.get("subscription_name") == "test_value"
assert value_sub.get("subscription_name_sanitized") == "test_value"
assert value_sub.get("subscription_value") == "is_overwritten"
assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@ -229,11 +250,13 @@ def test_subscription_file_value_applies_from_config_and_nested(
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@ -262,6 +285,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2"
@ -269,6 +293,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2"
@ -299,6 +324,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2"
@ -307,6 +333,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2"
@ -314,6 +341,24 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert "subscription_indent_3" not in sub_2_1
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line_old_format_errors(
config_file_with_subscription_value: ConfigFile,
preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors: Dict,
):
with mock_load_yaml(
preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors
), pytest.raises(
ValidationException,
match=re.escape(
"Validation error in parent_preset_2.=INDENT_1: 'INDENT_3' in '= INDENT_2 | INDENT_3' is not a preset name. "
"To use as a subscription indent value, define it as '= INDENT_3'"
),
):
Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked"
)
def test_subscription_file_bad_value(config_file: ConfigFile):
with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises(
ValidationException,
@ -346,7 +391,7 @@ def test_subscription_file_using_conflicting_preset_name(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"Validation error in sub_name: should be of type object."),
match=re.escape(f"Subscription value should either be a string, list, or object"),
):
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
@ -363,6 +408,7 @@ def test_tv_show_subscriptions(
jake_train_overrides = subs[2].overrides.dict_with_format_strings
assert jake_train_overrides["subscription_name"] == "Jake Trains"
assert jake_train_overrides["subscription_name_sanitized"] == "Jake Trains"
assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains"
assert jake_train_overrides["subscription_indent_1"] == "Kids"
assert jake_train_overrides["subscription_indent_2"] == "TV-Y"
@ -378,6 +424,7 @@ def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_pat
monk = subs[2].overrides.dict_with_format_strings
assert monk["subscription_name"] == "Stan Getz"
assert monk["subscription_name_sanitized"] == "Stan Getz"
assert monk["subscription_value"] == "https://www.youtube.com/@stangetzofficial/releases"
assert monk["subscription_indent_1"] == "Jazz"
@ -392,6 +439,7 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc
monk = subs[1].overrides.dict_with_format_strings
assert monk["subscription_name"] == "Michael Jackson"
assert monk["subscription_name_sanitized"] == "Michael Jackson"
assert (
monk["subscription_value"]
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"