Add aliases for ytdl-sub dl (#68)

* Add aliases for `ytdl-sub dl`

* error tests
This commit is contained in:
Jesse Bannon 2022-06-04 16:58:50 -07:00 committed by GitHub
parent 67104bca43
commit e111ae53f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 11 deletions

View file

@ -1,10 +1,13 @@
import hashlib
import shlex
from argparse import ArgumentError
from typing import Dict
from typing import List
from mergedeep import mergedeep
from ytdl_sub.config.config_file import ConfigOptions
class DownloadArgsParser:
"""
@ -14,11 +17,17 @@ class DownloadArgsParser:
: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._config_options = config_options
@property
def _argument_exception(self) -> ArgumentError:
@ -27,8 +36,7 @@ class DownloadArgsParser:
"""
return ArgumentError(
argument=None,
message="dl arguments must be in the form of --subscription.option.name 'value' "
"proceeding the url",
message="dl arguments must be in the form of --subscription.option.name 'value'",
)
@classmethod
@ -39,6 +47,20 @@ class DownloadArgsParser:
"""
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
def _argument_name_and_value_to_dict(cls, arg_name: str, arg_value: str) -> Dict:
"""
@ -49,7 +71,7 @@ class DownloadArgsParser:
argument_dict = {}
# 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
for next_arg_name in arg_name_split[:-1]:
@ -60,18 +82,36 @@ class DownloadArgsParser:
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:
"""
Converts the extra arguments into a dict equivalent to a subscription yaml.
:return: dict containing argument names and values
"""
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
for idx in range(0, len(self._unknown_arguments), 2):
argument_name = self._unknown_arguments[idx]
argument_value = self._unknown_arguments[idx + 1]
for idx in range(0, len(arguments), 2):
argument_name, argument_value = arguments[idx], arguments[idx + 1]
if not self._is_argument_name(arg=argument_name):
raise self._argument_exception

View file

@ -1,5 +1,6 @@
import os
from typing import Any
from typing import Dict
from typing import Optional
from ytdl_sub.utils.yaml import load_yaml
@ -10,7 +11,7 @@ from ytdl_sub.validators.validators import StringValidator
class ConfigOptions(StrictDictValidator):
_required_keys = {"working_directory"}
_optional_keys = {"umask"}
_optional_keys = {"umask", "dl_aliases"}
def __init__(self, name: str, value: Any):
super().__init__(name, value)
@ -21,6 +22,9 @@ class ConfigOptions(StrictDictValidator):
self._umask = self._validate_key_if_present(
key="umask", validator=StringValidator, default="022"
)
self._dl_aliases = self._validate_key_if_present(
key="dl_aliases", validator=LiteralDictValidator
)
@property
def working_directory(self) -> str:
@ -37,6 +41,34 @@ class ConfigOptions(StrictDictValidator):
"""
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):
_required_keys = {"configuration", "presets"}

View file

@ -40,7 +40,9 @@ def _download_subscription_from_cli(config: ConfigFile, extra_args: List[str]) -
:param config: Configuration file
: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_name = f"cli-dl-{dl_args_parser.get_args_hash()}"

View file

View 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()