[FEATURE] List support for ytdl-sub dl (#125)
* List support for ytdl-sub dl (#103) * Add test and amend docstrings
This commit is contained in:
parent
962b26d1ec
commit
287ac821dc
3 changed files with 165 additions and 13 deletions
|
|
@ -1,12 +1,13 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
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
|
from ytdl_sub.config.config_file import ConfigOptions
|
||||||
|
from ytdl_sub.utils.exceptions import InvalidDlArguments
|
||||||
|
|
||||||
|
|
||||||
class DownloadArgsParser:
|
class DownloadArgsParser:
|
||||||
|
|
@ -30,13 +31,12 @@ class DownloadArgsParser:
|
||||||
self._config_options = config_options
|
self._config_options = config_options
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _argument_exception(self) -> ArgumentError:
|
def _argument_exception(self) -> InvalidDlArguments:
|
||||||
"""
|
"""
|
||||||
:return: Exception to raise if a parsing error occurs.
|
:return: Exception to raise if a parsing error occurs.
|
||||||
"""
|
"""
|
||||||
return ArgumentError(
|
return InvalidDlArguments(
|
||||||
argument=None,
|
"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'",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -61,6 +61,46 @@ class DownloadArgsParser:
|
||||||
"""
|
"""
|
||||||
return arg_name[2:]
|
return arg_name[2:]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _uses_list(cls, argument_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
:param argument_name: The argument name which might be using list
|
||||||
|
:return: True if the argument uses list, denoted by ending with '[<positive integer>]'.
|
||||||
|
False otherwise.
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r"\[[1-9]\d*\]$")
|
||||||
|
|
||||||
|
return bool(pattern.search(argument_name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_list_index(cls, argument_name: str) -> int:
|
||||||
|
"""
|
||||||
|
:param argument_name: The argument name using list
|
||||||
|
:return: Splits the argument and returns the name and the list index.
|
||||||
|
"""
|
||||||
|
prefix, _, last = argument_name.rpartition("[")
|
||||||
|
|
||||||
|
return prefix, int(last[:-1]) - 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _find_largest_consecutive(cls, indices: List[int]) -> int:
|
||||||
|
"""
|
||||||
|
:param indices: List of indices
|
||||||
|
:return: Largest integer n + 1 so that all the integers from 0 up to n exist
|
||||||
|
in the indices.
|
||||||
|
0 if indices list is empty or the smallest integer in indices is larger than 0.
|
||||||
|
"""
|
||||||
|
largest_consecutive = -1
|
||||||
|
indices.sort()
|
||||||
|
|
||||||
|
for idx in indices:
|
||||||
|
if idx - largest_consecutive <= 1:
|
||||||
|
largest_consecutive = idx
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return largest_consecutive + 1
|
||||||
|
|
||||||
@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:
|
||||||
"""
|
"""
|
||||||
|
|
@ -98,28 +138,82 @@ class DownloadArgsParser:
|
||||||
|
|
||||||
return applied_alias_arguments
|
return applied_alias_arguments
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make_lists(cls, arguments: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Assemble lists
|
||||||
|
"""
|
||||||
|
list_arguments = {}
|
||||||
|
new_arguments = []
|
||||||
|
|
||||||
|
for idx in range(0, len(arguments), 2):
|
||||||
|
argument_name, argument_value = arguments[idx], arguments[idx + 1]
|
||||||
|
|
||||||
|
if not cls._uses_list(argument_name=argument_name):
|
||||||
|
new_arguments.append(argument_name)
|
||||||
|
new_arguments.append(argument_value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
argument_name, list_index = cls._extract_list_index(argument_name=argument_name)
|
||||||
|
|
||||||
|
if argument_name not in list_arguments:
|
||||||
|
list_arguments[argument_name] = ([], [])
|
||||||
|
|
||||||
|
list_arguments[argument_name][0].append(list_index)
|
||||||
|
list_arguments[argument_name][1].append(argument_value)
|
||||||
|
|
||||||
|
for name, (indices, values) in list_arguments.items():
|
||||||
|
list_length = cls._find_largest_consecutive(indices=indices.copy())
|
||||||
|
|
||||||
|
if list_length == 0:
|
||||||
|
raise InvalidDlArguments("Incomplete list")
|
||||||
|
|
||||||
|
list_value = [""] * list_length
|
||||||
|
|
||||||
|
for idx, value in zip(indices, values):
|
||||||
|
if idx < list_length:
|
||||||
|
list_value[idx] = value
|
||||||
|
|
||||||
|
new_arguments.append(name)
|
||||||
|
new_arguments.append(list_value)
|
||||||
|
|
||||||
|
return new_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.
|
||||||
|
:raises InvalidDlArguments: when an argument tries to use both list and non-list values.
|
||||||
:return: dict containing argument names and values
|
:return: dict containing argument names and values
|
||||||
"""
|
"""
|
||||||
subscription_dict = {}
|
|
||||||
arguments = self._apply_aliases(
|
arguments = self._apply_aliases(
|
||||||
unknown_arguments=self._unknown_arguments, aliases=self._config_options.dl_aliases
|
unknown_arguments=self._unknown_arguments, aliases=self._config_options.dl_aliases
|
||||||
)
|
)
|
||||||
if len(arguments) % 2 != 0:
|
if len(arguments) % 2 != 0:
|
||||||
raise self._argument_exception
|
raise self._argument_exception
|
||||||
|
|
||||||
|
if not all(
|
||||||
|
(self._is_argument_name(arg=arguments[idx]) for idx in range(0, len(arguments), 2))
|
||||||
|
):
|
||||||
|
raise self._argument_exception
|
||||||
|
|
||||||
|
arguments = self._make_lists(arguments=arguments)
|
||||||
|
subscription_dict = {}
|
||||||
|
|
||||||
for idx in range(0, len(arguments), 2):
|
for idx in range(0, len(arguments), 2):
|
||||||
argument_name, argument_value = arguments[idx], arguments[idx + 1]
|
argument_name, argument_value = arguments[idx], arguments[idx + 1]
|
||||||
|
|
||||||
if not self._is_argument_name(arg=argument_name):
|
|
||||||
raise self._argument_exception
|
|
||||||
|
|
||||||
argument_dict = self._argument_name_and_value_to_dict(
|
argument_dict = self._argument_name_and_value_to_dict(
|
||||||
arg_name=argument_name, arg_value=argument_value
|
arg_name=argument_name, arg_value=argument_value
|
||||||
)
|
)
|
||||||
mergedeep.merge(subscription_dict, argument_dict, strategy=mergedeep.Strategy.REPLACE)
|
try:
|
||||||
|
mergedeep.merge(
|
||||||
|
subscription_dict, argument_dict, strategy=mergedeep.Strategy.TYPESAFE_REPLACE
|
||||||
|
)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise InvalidDlArguments(
|
||||||
|
f"Invalid dl argument {argument_name}: "
|
||||||
|
"Cannot specify an argument to be two different types"
|
||||||
|
) from exc
|
||||||
|
|
||||||
return subscription_dict
|
return subscription_dict
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,7 @@ class InvalidYamlException(ValidationException):
|
||||||
|
|
||||||
class RegexNoMatchException(ValidationException):
|
class RegexNoMatchException(ValidationException):
|
||||||
"""Regex failed to match during download"""
|
"""Regex failed to match during download"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDlArguments(ValidationException):
|
||||||
|
"""dl arguments that are invalid"""
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import shlex
|
import shlex
|
||||||
from argparse import ArgumentError
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -10,6 +9,7 @@ import pytest
|
||||||
from ytdl_sub.cli.download_args_parser import DownloadArgsParser
|
from ytdl_sub.cli.download_args_parser import DownloadArgsParser
|
||||||
from ytdl_sub.cli.main_args_parser import parser
|
from ytdl_sub.cli.main_args_parser import parser
|
||||||
from ytdl_sub.config.config_file import ConfigOptions
|
from ytdl_sub.config.config_file import ConfigOptions
|
||||||
|
from ytdl_sub.utils.exceptions import InvalidDlArguments
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -66,6 +66,36 @@ class TestDownloadArgsParser:
|
||||||
"dl --youtube.playlist_url https://youtube.com/playlist?list=123abc",
|
"dl --youtube.playlist_url https://youtube.com/playlist?list=123abc",
|
||||||
{"youtube": {"playlist_url": "https://youtube.com/playlist?list=123abc"}},
|
{"youtube": {"playlist_url": "https://youtube.com/playlist?list=123abc"}},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.using.list[1] 'v1'",
|
||||||
|
{"parameter": {"using": {"list": ["v1"]}}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.using.list[1] 'v1' --parameter.using.list[2] 'v2'",
|
||||||
|
{"parameter": {"using": {"list": ["v1", "v2"]}}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.using.list[2] 'v2' --parameter.using.list[1] 'v1'",
|
||||||
|
{"parameter": {"using": {"list": ["v1", "v2"]}}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.using.list[2] 'v3' --parameter.using.list[1] 'v1' --parameter.using.list[2] 'v2'",
|
||||||
|
{"parameter": {"using": {"list": ["v1", "v2"]}}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.using.list[3] 'v3' --parameter.using.list[1] 'v1'",
|
||||||
|
{"parameter": {"using": {"list": ["v1"]}}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
"dl --parameter.not.using.list[0] 'v0'",
|
||||||
|
{"parameter": {"not": {"using": {"list[0]": "v0"}}}},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_successful_args(self, config_options_generator, aliases, cmd, expected_sub_dict):
|
def test_successful_args(self, config_options_generator, aliases, cmd, expected_sub_dict):
|
||||||
|
|
@ -82,7 +112,7 @@ class TestDownloadArgsParser:
|
||||||
config_options = config_options_generator()
|
config_options = config_options_generator()
|
||||||
extra_args = _get_extra_arguments(cmd_string="dl --preset")
|
extra_args = _get_extra_arguments(cmd_string="dl --preset")
|
||||||
|
|
||||||
with pytest.raises(ArgumentError, match=argument_error_msg):
|
with pytest.raises(InvalidDlArguments, match=argument_error_msg):
|
||||||
DownloadArgsParser(
|
DownloadArgsParser(
|
||||||
extra_arguments=extra_args, config_options=config_options
|
extra_arguments=extra_args, config_options=config_options
|
||||||
).to_subscription_dict()
|
).to_subscription_dict()
|
||||||
|
|
@ -91,7 +121,31 @@ class TestDownloadArgsParser:
|
||||||
config_options = config_options_generator()
|
config_options = config_options_generator()
|
||||||
extra_args = _get_extra_arguments(cmd_string="dl --preset buttered toast --test")
|
extra_args = _get_extra_arguments(cmd_string="dl --preset buttered toast --test")
|
||||||
|
|
||||||
with pytest.raises(ArgumentError, match=argument_error_msg):
|
with pytest.raises(InvalidDlArguments, match=argument_error_msg):
|
||||||
|
DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config_options
|
||||||
|
).to_subscription_dict()
|
||||||
|
|
||||||
|
def test_error_incomplete_list(self, config_options_generator):
|
||||||
|
config_options = config_options_generator()
|
||||||
|
extra_args = _get_extra_arguments(cmd_string="dl --parameter.using.list[3] 'v3'")
|
||||||
|
|
||||||
|
with pytest.raises(InvalidDlArguments, match="Incomplete list"):
|
||||||
|
DownloadArgsParser(
|
||||||
|
extra_arguments=extra_args, config_options=config_options
|
||||||
|
).to_subscription_dict()
|
||||||
|
|
||||||
|
def test_error_two_different_types(self, config_options_generator):
|
||||||
|
config_options = config_options_generator()
|
||||||
|
extra_args = _get_extra_arguments(
|
||||||
|
cmd_string="dl --parameter.using.list 'v2' --parameter.using.list[1] 'v1'"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
InvalidDlArguments,
|
||||||
|
match=f"Invalid dl argument --parameter.using.list: "
|
||||||
|
"Cannot specify an argument to be two different types",
|
||||||
|
):
|
||||||
DownloadArgsParser(
|
DownloadArgsParser(
|
||||||
extra_arguments=extra_args, config_options=config_options
|
extra_arguments=extra_args, config_options=config_options
|
||||||
).to_subscription_dict()
|
).to_subscription_dict()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue