Add aliases for ytdl-sub dl (#68)
* Add aliases for `ytdl-sub dl` * error tests
This commit is contained in:
parent
67104bca43
commit
e111ae53f5
5 changed files with 178 additions and 11 deletions
|
|
@ -1,10 +1,13 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import shlex
|
||||||
from argparse import ArgumentError
|
from argparse import ArgumentError
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from mergedeep import mergedeep
|
from mergedeep import mergedeep
|
||||||
|
|
||||||
|
from ytdl_sub.config.config_file import ConfigOptions
|
||||||
|
|
||||||
|
|
||||||
class DownloadArgsParser:
|
class DownloadArgsParser:
|
||||||
"""
|
"""
|
||||||
|
|
@ -14,11 +17,17 @@ class DownloadArgsParser:
|
||||||
:class:`~ytdl_subscribe.validators.config.preset_validator.PresetValidator`
|
:class:`~ytdl_subscribe.validators.config.preset_validator.PresetValidator`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, extra_arguments: List[str]):
|
def __init__(self, extra_arguments: List[str], config_options: ConfigOptions):
|
||||||
"""
|
"""
|
||||||
:param extra_arguments: List of extra arguments from argparse
|
Parameters
|
||||||
|
----------
|
||||||
|
extra_arguments
|
||||||
|
List of extra arguments from argparse
|
||||||
|
config_options
|
||||||
|
Configuration portion of config.yaml
|
||||||
"""
|
"""
|
||||||
self._unknown_arguments = extra_arguments
|
self._unknown_arguments = extra_arguments
|
||||||
|
self._config_options = config_options
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _argument_exception(self) -> ArgumentError:
|
def _argument_exception(self) -> ArgumentError:
|
||||||
|
|
@ -27,8 +36,7 @@ class DownloadArgsParser:
|
||||||
"""
|
"""
|
||||||
return ArgumentError(
|
return ArgumentError(
|
||||||
argument=None,
|
argument=None,
|
||||||
message="dl arguments must be in the form of --subscription.option.name 'value' "
|
message="dl arguments must be in the form of --subscription.option.name 'value'",
|
||||||
"proceeding the url",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -39,6 +47,20 @@ class DownloadArgsParser:
|
||||||
"""
|
"""
|
||||||
return arg.startswith("--")
|
return arg.startswith("--")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_argument_name(cls, arg_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
arg_name
|
||||||
|
Argument name
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The argument name with the -- removed
|
||||||
|
"""
|
||||||
|
return arg_name[2:]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _argument_name_and_value_to_dict(cls, arg_name: str, arg_value: str) -> Dict:
|
def _argument_name_and_value_to_dict(cls, arg_name: str, arg_value: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -49,7 +71,7 @@ class DownloadArgsParser:
|
||||||
argument_dict = {}
|
argument_dict = {}
|
||||||
|
|
||||||
# Remove the argument --'s, then split on period
|
# Remove the argument --'s, then split on period
|
||||||
arg_name_split = arg_name.replace("--", "", 1).split(".")
|
arg_name_split = cls._get_argument_name(arg_name).split(".")
|
||||||
|
|
||||||
next_dict = argument_dict
|
next_dict = argument_dict
|
||||||
for next_arg_name in arg_name_split[:-1]:
|
for next_arg_name in arg_name_split[:-1]:
|
||||||
|
|
@ -60,18 +82,36 @@ class DownloadArgsParser:
|
||||||
|
|
||||||
return argument_dict
|
return argument_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _apply_aliases(cls, unknown_arguments: List[str], aliases: Dict) -> List[str]:
|
||||||
|
"""
|
||||||
|
Applies any aliases from the config to the unknown arguments
|
||||||
|
"""
|
||||||
|
applied_alias_arguments: List[str] = []
|
||||||
|
|
||||||
|
for arg in unknown_arguments:
|
||||||
|
if cls._is_argument_name(arg) and cls._get_argument_name(arg) in aliases:
|
||||||
|
arg = aliases[cls._get_argument_name(arg)]
|
||||||
|
applied_alias_arguments.extend(shlex.split(arg))
|
||||||
|
else:
|
||||||
|
applied_alias_arguments.append(arg)
|
||||||
|
|
||||||
|
return applied_alias_arguments
|
||||||
|
|
||||||
def to_subscription_dict(self) -> Dict:
|
def to_subscription_dict(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Converts the extra arguments into a dict equivalent to a subscription yaml.
|
Converts the extra arguments into a dict equivalent to a subscription yaml.
|
||||||
:return: dict containing argument names and values
|
:return: dict containing argument names and values
|
||||||
"""
|
"""
|
||||||
subscription_dict = {}
|
subscription_dict = {}
|
||||||
if len(self._unknown_arguments) % 2 != 0:
|
arguments = self._apply_aliases(
|
||||||
|
unknown_arguments=self._unknown_arguments, aliases=self._config_options.dl_aliases
|
||||||
|
)
|
||||||
|
if len(arguments) % 2 != 0:
|
||||||
raise self._argument_exception
|
raise self._argument_exception
|
||||||
|
|
||||||
for idx in range(0, len(self._unknown_arguments), 2):
|
for idx in range(0, len(arguments), 2):
|
||||||
argument_name = self._unknown_arguments[idx]
|
argument_name, argument_value = arguments[idx], arguments[idx + 1]
|
||||||
argument_value = self._unknown_arguments[idx + 1]
|
|
||||||
|
|
||||||
if not self._is_argument_name(arg=argument_name):
|
if not self._is_argument_name(arg=argument_name):
|
||||||
raise self._argument_exception
|
raise self._argument_exception
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ytdl_sub.utils.yaml import load_yaml
|
from ytdl_sub.utils.yaml import load_yaml
|
||||||
|
|
@ -10,7 +11,7 @@ from ytdl_sub.validators.validators import StringValidator
|
||||||
|
|
||||||
class ConfigOptions(StrictDictValidator):
|
class ConfigOptions(StrictDictValidator):
|
||||||
_required_keys = {"working_directory"}
|
_required_keys = {"working_directory"}
|
||||||
_optional_keys = {"umask"}
|
_optional_keys = {"umask", "dl_aliases"}
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __init__(self, name: str, value: Any):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
@ -21,6 +22,9 @@ class ConfigOptions(StrictDictValidator):
|
||||||
self._umask = self._validate_key_if_present(
|
self._umask = self._validate_key_if_present(
|
||||||
key="umask", validator=StringValidator, default="022"
|
key="umask", validator=StringValidator, default="022"
|
||||||
)
|
)
|
||||||
|
self._dl_aliases = self._validate_key_if_present(
|
||||||
|
key="dl_aliases", validator=LiteralDictValidator
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def working_directory(self) -> str:
|
def working_directory(self) -> str:
|
||||||
|
|
@ -37,6 +41,34 @@ class ConfigOptions(StrictDictValidator):
|
||||||
"""
|
"""
|
||||||
return self._umask.value
|
return self._umask.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dl_aliases(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Alias definitions to shorten ``ytdl-sub dl`` arguments. For example,
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
dl_aliases:
|
||||||
|
mv: "--preset yt_music_video"
|
||||||
|
v: "--youtube.video_id"
|
||||||
|
|
||||||
|
Simplifies
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
ytdl-sub dl --preset yt_music_video --youtube.video_id a1b2c3
|
||||||
|
|
||||||
|
to
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
ytdl-sub dl --mv --v a1b2c3
|
||||||
|
"""
|
||||||
|
if self._dl_aliases:
|
||||||
|
return self._dl_aliases.dict
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class ConfigFile(StrictDictValidator):
|
class ConfigFile(StrictDictValidator):
|
||||||
_required_keys = {"configuration", "presets"}
|
_required_keys = {"configuration", "presets"}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ def _download_subscription_from_cli(config: ConfigFile, extra_args: List[str]) -
|
||||||
:param config: Configuration file
|
:param config: Configuration file
|
||||||
:param extra_args: Extra arguments from argparse that contain dynamic subscription options
|
:param extra_args: Extra arguments from argparse that contain dynamic subscription options
|
||||||
"""
|
"""
|
||||||
dl_args_parser = DownloadArgsParser(extra_arguments=extra_args)
|
dl_args_parser = DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config.config_options
|
||||||
|
)
|
||||||
subscription_args_dict = dl_args_parser.to_subscription_dict()
|
subscription_args_dict = dl_args_parser.to_subscription_dict()
|
||||||
|
|
||||||
subscription_name = f"cli-dl-{dl_args_parser.get_args_hash()}"
|
subscription_name = f"cli-dl-{dl_args_parser.get_args_hash()}"
|
||||||
|
|
|
||||||
0
tests/unit/cli/__init__.py
Normal file
0
tests/unit/cli/__init__.py
Normal file
93
tests/unit/cli/test_download_args_parser.py
Normal file
93
tests/unit/cli/test_download_args_parser.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import shlex
|
||||||
|
from argparse import ArgumentError
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ytdl_sub.cli.download_args_parser import DownloadArgsParser
|
||||||
|
from ytdl_sub.cli.main_args_parser import parser
|
||||||
|
from ytdl_sub.config.config_file import ConfigOptions
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_options_generator() -> Callable:
|
||||||
|
def _config_options_generator(dl_aliases: Optional[Dict[str, str]] = None):
|
||||||
|
config_options_dict = {
|
||||||
|
"working_directory": ".",
|
||||||
|
}
|
||||||
|
if dl_aliases:
|
||||||
|
config_options_dict["dl_aliases"] = dl_aliases
|
||||||
|
|
||||||
|
return ConfigOptions(name="config_options_unit_test", value=config_options_dict)
|
||||||
|
|
||||||
|
return _config_options_generator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def argument_error_msg() -> str:
|
||||||
|
return "dl arguments must be in the form of --subscription.option.name 'value'"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_extra_arguments(cmd_string: str) -> List[str]:
|
||||||
|
sys_argv = shlex.split(cmd_string)
|
||||||
|
_, extra_args = parser.parse_known_args(args=sys_argv)
|
||||||
|
|
||||||
|
return extra_args
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownloadArgsParser:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"aliases, cmd, expected_sub_dict",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"mv": "--preset yt_music_video", "v": "--youtube.video_id"},
|
||||||
|
"dl --mv --v 123abc",
|
||||||
|
{"preset": "yt_music_video", "youtube": {"video_id": "123abc"}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"ch": "--preset yt_channel",
|
||||||
|
"c": "--youtube.channel_id",
|
||||||
|
"name": "--overrides.tv_name",
|
||||||
|
"concert": "--overrides.genre 'Some Genre Name'",
|
||||||
|
},
|
||||||
|
"dl --ch --c 123abc --name 'Sweet TV Show' --concert",
|
||||||
|
{
|
||||||
|
"preset": "yt_channel",
|
||||||
|
"youtube": {"channel_id": "123abc"},
|
||||||
|
"overrides": {"tv_name": "Sweet TV Show", "genre": "Some Genre Name"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(None, "dl --youtube.playlist_id 123abc", {"youtube": {"playlist_id": "123abc"}}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_successful_args(self, config_options_generator, aliases, cmd, expected_sub_dict):
|
||||||
|
config_options = config_options_generator(dl_aliases=aliases)
|
||||||
|
extra_args = _get_extra_arguments(cmd_string=cmd)
|
||||||
|
|
||||||
|
output_sub_dict = DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config_options
|
||||||
|
).to_subscription_dict()
|
||||||
|
|
||||||
|
assert output_sub_dict == expected_sub_dict
|
||||||
|
|
||||||
|
def test_error_uneven_args(self, config_options_generator, argument_error_msg):
|
||||||
|
config_options = config_options_generator()
|
||||||
|
extra_args = _get_extra_arguments(cmd_string="dl --preset")
|
||||||
|
|
||||||
|
with pytest.raises(ArgumentError, match=argument_error_msg):
|
||||||
|
DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config_options
|
||||||
|
).to_subscription_dict()
|
||||||
|
|
||||||
|
def test_error_adjacent_values(self, config_options_generator, argument_error_msg):
|
||||||
|
config_options = config_options_generator()
|
||||||
|
extra_args = _get_extra_arguments(cmd_string="dl --preset buttered toast --test")
|
||||||
|
|
||||||
|
with pytest.raises(ArgumentError, match=argument_error_msg):
|
||||||
|
DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config_options
|
||||||
|
).to_subscription_dict()
|
||||||
Loading…
Reference in a new issue