diff --git a/src/ytdl_sub/cli/main_args_parser.py b/src/ytdl_sub/cli/main_args_parser.py index fc626e0d..dff72a68 100644 --- a/src/ytdl_sub/cli/main_args_parser.py +++ b/src/ytdl_sub/cli/main_args_parser.py @@ -22,7 +22,7 @@ subscription_parser.add_argument( metavar="SUBPATH", nargs="*", help="path to subscription files, uses subscriptions.yaml if not provided", - default="subscriptions.yaml", + default=["subscriptions.yaml"], ) ################################################################################################### # DOWNLOAD PARSER diff --git a/src/ytdl_sub/config/config_file.py b/src/ytdl_sub/config/config_file.py index 5e975ba7..7d9d5528 100644 --- a/src/ytdl_sub/config/config_file.py +++ b/src/ytdl_sub/config/config_file.py @@ -1,7 +1,6 @@ from typing import Any -import yaml - +from ytdl_sub.utils.yaml import load_yaml from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import StringValidator @@ -56,7 +55,5 @@ class ConfigFile(StrictDictValidator): ------- Config file validator """ - # TODO: Create separate yaml file loader class - with open(config_path, "r", encoding="utf-8") as file: - config_dict = yaml.safe_load(file) + config_dict = load_yaml(file_path=config_path) return ConfigFile.from_dict(config_dict) diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index 68a5d281..f1c9c76d 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -6,7 +6,6 @@ from typing import Optional from typing import Tuple from typing import Type -import yaml from mergedeep import mergedeep from ytdl_sub.config.config_file import ConfigFile @@ -20,6 +19,7 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException +from ytdl_sub.utils.yaml import load_yaml from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.validators import DictValidator @@ -280,9 +280,7 @@ class Preset(StrictDictValidator): ------- List of presets, for each one in the subscription yaml """ - # TODO: Create separate yaml file loader class - with open(subscription_path, "r", encoding="utf-8") as file: - subscription_dict = yaml.safe_load(file) + subscription_dict = load_yaml(file_path=subscription_path) subscriptions: List["Preset"] = [] for subscription_key, subscription_object in subscription_dict.items(): diff --git a/src/ytdl_sub/utils/exceptions.py b/src/ytdl_sub/utils/exceptions.py index 0f20ed14..9574c430 100644 --- a/src/ytdl_sub/utils/exceptions.py +++ b/src/ytdl_sub/utils/exceptions.py @@ -12,3 +12,11 @@ class StringFormattingVariableNotFoundException(StringFormattingException): class DownloadArchiveException(ValueError): """Any user or file errors caused by download archive or mapping files""" + + +class FileNotFoundException(ValidationException): + """Any user file that's expected to exist, but is not found""" + + +class InvalidYamlException(ValidationException): + """User yaml that is invalid""" diff --git a/src/ytdl_sub/utils/yaml.py b/src/ytdl_sub/utils/yaml.py new file mode 100644 index 00000000..0556133c --- /dev/null +++ b/src/ytdl_sub/utils/yaml.py @@ -0,0 +1,38 @@ +import os.path +from typing import Dict + +import yaml +from yaml import YAMLError + +from ytdl_sub.utils.exceptions import FileNotFoundException +from ytdl_sub.utils.exceptions import InvalidYamlException + + +def load_yaml(file_path: str) -> Dict: + """ + Parameters + ---------- + file_path + Path of the file + + Returns + ------- + Dict representation of the yaml file + + Raises + ------ + FileNotFoundException + The file does not exist + InvalidYamlException + The file has invalid yaml + """ + if not os.path.isfile(file_path): + raise FileNotFoundException(f"The file '{file_path}' does not exist.") + + try: + with open(file_path, "r", encoding="utf-8") as file: + return yaml.safe_load(file) + except YAMLError as yaml_exception: + raise InvalidYamlException( + f"'{file_path}' has invalid YAML, copy-paste it into a YAML checker to find the issue." + ) from yaml_exception diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/utils/test_yaml.py b/tests/unit/utils/test_yaml.py new file mode 100644 index 00000000..1ccdbee2 --- /dev/null +++ b/tests/unit/utils/test_yaml.py @@ -0,0 +1,41 @@ +import tempfile + +import pytest + +from ytdl_sub.utils.exceptions import FileNotFoundException +from ytdl_sub.utils.exceptions import InvalidYamlException +from ytdl_sub.utils.yaml import load_yaml + + +@pytest.fixture +def bad_yaml(): + return """ + this: + is: + bad: + asdf: + bad : + :yaml + """ + + +@pytest.fixture +def bad_yaml_file_path(bad_yaml): + with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp_file: + tmp_file.write(bad_yaml.encode("utf-8")) + tmp_file.flush() + yield tmp_file.name + + +def test_load_yaml_file_not_found(): + file_path = "does_not_exist.yaml" + with pytest.raises(FileNotFoundException, match=f"The file '{file_path}' does not exist."): + load_yaml(file_path=file_path) + + +def test_load_yaml_invalid_syntax(bad_yaml_file_path): + with pytest.raises( + InvalidYamlException, + match=f"'{bad_yaml_file_path}' has invalid YAML, copy-paste it into a YAML checker to find the issue.", + ): + load_yaml(file_path=bad_yaml_file_path)