From 9f408d219630799af538a644362cd3299b447e44 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Wed, 1 Nov 2023 20:24:06 -0700 Subject: [PATCH] [FEATURE] Extend subscription syntax (#790) Extends the subscription.yaml syntax at the cost of introducing a breaking change. # New Syntax ## Mix-n-match presets and indent variables ``` # Can mix/match presets and indent override variables. # Uses presets TV Show, Only Recent and assigns Kids, TV-Y to subscription_indent_1 and _2 TV Show | = Kids | = TV-Y | Only Recent: "Jake Trains": "https://..." ``` ## Subscriptions with list-value support ``` TV Show | = Kids | = TV-Y | Only Recent: "Jake Trains": - "https://url.1..." # Assigns to subscription_value and subscription_value_1 - "https://url.2..." # Assigns to subscription_value_2 ``` ## Subscriptions with override-keys support ``` TV Show | = Kids | = TV-Y | Only Recent: "~Jake Trains": # the ~ means "all keys underneath get assigned as override variables" url: "https://url.1..." # Assigns to url url2: "https://url.2..." # Assigns to url2 ``` # Breaking Changes In the TV show subscriptions example (https://github.com/jmbannon/ytdl-sub/blob/master/examples/tv_show_subscriptions.yaml), it had ``` TV Show Full Archive: # Sets "Kids" for genre, "TV-Y" for content rating = Kids | TV-Y: "Jake Trains": "https://www.youtube.com/@JakeTrains" "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" ``` This must be changed to ``` TV Show Full Archive: # Sets "Kids" for genre, "TV-Y" for content rating = Kids | = TV-Y: # Each indent variable assignment must have an = before it "Jake Trains": "https://www.youtube.com/@JakeTrains" "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" ``` --- examples/tv_show_subscriptions.yaml | 4 +- src/ytdl_sub/config/config_validator.py | 5 +- src/ytdl_sub/config/preset_options.py | 66 ++--- .../subscriptions/subscription_validators.py | 258 +++++++++++++----- src/ytdl_sub/subscriptions/utils.py | 33 +++ tests/unit/config/test_subscription.py | 149 +++++++++- 6 files changed, 397 insertions(+), 118 deletions(-) create mode 100644 src/ytdl_sub/subscriptions/utils.py diff --git a/examples/tv_show_subscriptions.yaml b/examples/tv_show_subscriptions.yaml index 5c287227..c043cc4f 100644 --- a/examples/tv_show_subscriptions.yaml +++ b/examples/tv_show_subscriptions.yaml @@ -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" diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py index b2ed0aef..897684ea 100644 --- a/src/ytdl_sub/config/config_validator.py +++ b/src/ytdl_sub/config/config_validator.py @@ -10,6 +10,7 @@ from ytdl_sub.config.defaults import DEFAULT_FFPROBE_PATH from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS +from ytdl_sub.subscriptions.utils import SUBSCRIPTION_VALUE_CONFIG_KEY from ytdl_sub.validators.file_path_validators import FFmpegFileValidator from ytdl_sub.validators.file_path_validators import FFprobeFileValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator @@ -106,7 +107,7 @@ class ConfigOptions(StrictDictValidator): "ffprobe_path", "file_name_max_bytes", "experimental", - "subscription_value", + SUBSCRIPTION_VALUE_CONFIG_KEY, } def __init__(self, name: str, value: Any): @@ -142,7 +143,7 @@ class ConfigOptions(StrictDictValidator): key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES ) self._subscription_value = self._validate_key_if_present( - key="subscription_value", validator=StringValidator + key=SUBSCRIPTION_VALUE_CONFIG_KEY, validator=StringValidator ) @property diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index 858068f5..c159fa4d 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -9,6 +9,7 @@ from yt_dlp.utils import sanitize_filename from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME from ytdl_sub.entries.entry import Entry +from ytdl_sub.subscriptions.utils import SUBSCRIPTION_NAME from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator @@ -120,45 +121,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 +153,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) @@ -199,6 +174,23 @@ class Overrides(OverridesVariables): sanitize=True, ) + if SUBSCRIPTION_NAME not in self._value: + for sanitized in [True, False]: + self._add_override_variable( + key_name=SUBSCRIPTION_NAME, + format_string=self.subscription_name, + sanitize=sanitized, + ) + + @property + def subscription_name(self) -> str: + """ + Returns + ------- + Name of the subscription + """ + return self._root_name + def apply_formatter( self, formatter: StringFormatterValidator, diff --git a/src/ytdl_sub/subscriptions/subscription_validators.py b/src/ytdl_sub/subscriptions/subscription_validators.py index 1d3686a9..007b2b9a 100644 --- a/src/ytdl_sub/subscriptions/subscription_validators.py +++ b/src/ytdl_sub/subscriptions/subscription_validators.py @@ -1,53 +1,25 @@ 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.subscriptions.utils import SUBSCRIPTION_NAME +from ytdl_sub.subscriptions.utils import SUBSCRIPTION_VALUE +from ytdl_sub.subscriptions.utils import subscription_indent_variable_name +from ytdl_sub.subscriptions.utils import subscription_list_variable_name +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 from ytdl_sub.validators.validators import Validator -def subscription_indent_variable_name(index: int) -> str: - """ - Parameters - ---------- - index - 0th-based index - - Returns - ------- - subscription_index_i, where i is 1-based index - """ - return f"subscription_indent_{index + 1}" - - -def subscription_value_variable_name() -> str: - """ - Returns - ------- - The override variable name containing the subscription value if present - """ - 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 +47,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 +74,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 +123,73 @@ 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[subscription_list_variable_name(index=idx)] = 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 +197,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 +241,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 +254,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 +305,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] = {} diff --git a/src/ytdl_sub/subscriptions/utils.py b/src/ytdl_sub/subscriptions/utils.py new file mode 100644 index 00000000..3e2c9a00 --- /dev/null +++ b/src/ytdl_sub/subscriptions/utils.py @@ -0,0 +1,33 @@ +SUBSCRIPTION_NAME = "subscription_name" +SUBSCRIPTION_VALUE = "subscription_value" + +# Key used in configs, should delete at some point +SUBSCRIPTION_VALUE_CONFIG_KEY = "subscription_value" + + +def subscription_indent_variable_name(index: int) -> str: + """ + Parameters + ---------- + index + 0th-based index + + Returns + ------- + subscription_index_i, where i is 1-based index + """ + return f"subscription_indent_{index + 1}" + + +def subscription_list_variable_name(index: int) -> str: + """ + Parameters + ---------- + index + 0th-based index + + Returns + ------- + subscription_value_i, where i is 1-based index + """ + return f"subscription_value_{index + 1}" diff --git a/tests/unit/config/test_subscription.py b/tests/unit/config/test_subscription.py index 097e738c..a419a8d6 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -116,6 +116,72 @@ 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_all_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", + }, + "parent_preset_2 | =INDENT_1 |= INDENT_2 | = INDENT_3 ": { + "test_1": "is_1_overwritten", + }, + }, + ) + + +@pytest.fixture +def preset_with_subscription_list( + preset_with_subscription_value: Dict, +): + return dict( + preset_with_subscription_value, + **{ + "parent_preset_2 | parent_preset_1": { + "test_2_1": ["is_2_1_overwritten", "is_2_1_list_2"] + }, + }, + ) + + +@pytest.fixture +def preset_with_subscription_overrides_tilda( + preset_with_subscription_value: Dict, +): + return dict( + preset_with_subscription_value, + **{ + "parent_preset_2 | parent_preset_1": { + "~test_2_1": { + "current_override": "test_2_1", + } + }, + }, + ) + + +@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 +257,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 +276,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,15 +297,56 @@ 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 +def test_subscription_list( + config_file_with_subscription_value: ConfigFile, + preset_with_subscription_list: Dict, +): + with mock_load_yaml(preset_dict=preset_with_subscription_list): + subs = Subscription.from_file_path( + config=config_file_with_subscription_value, subscription_path="mocked" + ) + assert len(subs) == 3 + + # Test __value__ worked correctly from the config + sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings + + 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_value_1") == "is_2_1_overwritten" + assert sub_2_1.get("subscription_value_2") == "is_2_1_list_2" + assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence + + +def test_subscription_overrides_tilda( + config_file_with_subscription_value: ConfigFile, + preset_with_subscription_overrides_tilda: Dict, +): + with mock_load_yaml(preset_dict=preset_with_subscription_overrides_tilda): + subs = Subscription.from_file_path( + config=config_file_with_subscription_value, subscription_path="mocked" + ) + assert len(subs) == 3 + + # Test __value__ worked correctly from the config + sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings + + 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("current_override") == "test_2_1" # tilda sub takes precedence + + def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables( config_file_with_subscription_value: ConfigFile, preset_with_subscription_value_nested_presets_and_indent_variables: Dict, @@ -262,6 +371,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,19 +379,27 @@ 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" assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence +@pytest.mark.parametrize("all_same_line", [True, False]) def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line( config_file_with_subscription_value: ConfigFile, preset_with_subscription_value_nested_presets_and_indent_variables_same_line: Dict, + preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line: Dict, + all_same_line: bool, ): - with mock_load_yaml( - preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables_same_line - ): + preset_dict = preset_with_subscription_value_nested_presets_and_indent_variables_same_line + if all_same_line: + preset_dict = ( + preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line + ) + + with mock_load_yaml(preset_dict=preset_dict): subs = Subscription.from_file_path( config=config_file_with_subscription_value, subscription_path="mocked" ) @@ -299,6 +417,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 +426,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 +434,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 +484,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 +501,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 +517,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 +532,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"