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 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

View file

@ -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"}

View file

@ -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()}"

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