better recursion functionality
This commit is contained in:
parent
5fed8a7b64
commit
cbe73adbf7
5 changed files with 66 additions and 45 deletions
|
|
@ -8,6 +8,7 @@ from ytdl_subscribe.validators.base.string_formatter_validators import (
|
|||
from ytdl_subscribe.validators.config.overrides.overrides_validator import (
|
||||
OverridesValidator,
|
||||
)
|
||||
from ytdl_subscribe.validators.exceptions import StringFormattingException
|
||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||
|
||||
|
||||
|
|
@ -98,14 +99,16 @@ class TestEntry(object):
|
|||
},
|
||||
)
|
||||
|
||||
# Max depth is 3 so should go level_a -(0)-> level_b -(1)-> level_a -(2)-> level_b
|
||||
expected_error_msg = (
|
||||
f"Attempted to format 'test' but failed after reaching max recursion depth of 3. "
|
||||
f"Try to keep variables dependent on only one other variable."
|
||||
"Validation error in test: Attempted to format but failed after reaching max recursion "
|
||||
"depth of 3. Try to keep variables dependent on only one other variable at max. "
|
||||
"Unresolved variables: level_b"
|
||||
)
|
||||
|
||||
format_string = StringFormatterValidator(name="test", value="{level_a}")
|
||||
|
||||
with pytest.raises(ValidationException, match=expected_error_msg):
|
||||
with pytest.raises(StringFormattingException, match=expected_error_msg):
|
||||
_ = mock_entry.apply_formatter(format_string, overrides=overrides)
|
||||
|
||||
def test_entry_missing_kwarg(self, mock_entry):
|
||||
|
|
@ -121,7 +124,10 @@ class TestEntry(object):
|
|||
name="test", value=f"prefix {{bah_humbug}} suffix"
|
||||
)
|
||||
available_fields = ", ".join(sorted(mock_entry.to_dict().keys()))
|
||||
expected_error_msg = f"Format variable 'bah_humbug' does not exist for Entry. Available fields: {available_fields}"
|
||||
expected_error_msg = (
|
||||
f"Validation error in test: Format variable 'bah_humbug' does not exist. "
|
||||
f"Available variables: {available_fields}"
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=expected_error_msg):
|
||||
with pytest.raises(StringFormattingException, match=expected_error_msg):
|
||||
assert mock_entry.apply_formatter(format_string)
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ class Entry:
|
|||
Entry object to represent a single media object returned from yt-dlp.
|
||||
"""
|
||||
|
||||
_MAX_FORMATTER_RECURSION = 3
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initialize the entry using ytdl metadata
|
||||
|
|
@ -122,38 +120,6 @@ class Entry:
|
|||
"""
|
||||
entry_dict = self.to_dict()
|
||||
if overrides:
|
||||
# TODO: need to check recursively populate format variables
|
||||
entry_dict = dict(entry_dict, **overrides.dict_with_format_strings)
|
||||
|
||||
for field_name in formatter.format_variables:
|
||||
if field_name not in entry_dict:
|
||||
available_fields = ", ".join(sorted(entry_dict.keys()))
|
||||
raise ValueError(
|
||||
f"Format variable '{field_name}' does not exist "
|
||||
f"for {self.__class__.__name__}. Available fields: {available_fields}"
|
||||
)
|
||||
|
||||
format_string = formatter.format_string
|
||||
variables_present = True
|
||||
recursion_depth = 0
|
||||
while variables_present and recursion_depth < self._MAX_FORMATTER_RECURSION:
|
||||
format_string = format_string.format(**OrderedDict(entry_dict))
|
||||
variables_present = (
|
||||
len(
|
||||
StringFormatterValidator(
|
||||
name="__recursive_formatter_update__",
|
||||
value=format_string,
|
||||
).format_variables
|
||||
)
|
||||
> 0
|
||||
)
|
||||
recursion_depth += 1
|
||||
|
||||
if variables_present:
|
||||
raise ValidationException(
|
||||
f"Attempted to format '{formatter._name}' but failed after reaching max recursion "
|
||||
f"depth of {self._MAX_FORMATTER_RECURSION}. Try to keep variables dependent on "
|
||||
f"only one other variable."
|
||||
)
|
||||
|
||||
return format_string
|
||||
return formatter.apply_formatter(variable_dict=entry_dict)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
from collections import OrderedDict
|
||||
from keyword import iskeyword
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
|
@ -6,6 +7,7 @@ from typing import final
|
|||
|
||||
from ytdl_subscribe.validators.base.validators import LiteralDictValidator
|
||||
from ytdl_subscribe.validators.base.validators import Validator
|
||||
from ytdl_subscribe.validators.exceptions import StringFormattingException
|
||||
|
||||
|
||||
class StringFormatterValidator(Validator):
|
||||
|
|
@ -18,6 +20,8 @@ class StringFormatterValidator(Validator):
|
|||
|
||||
__fields_validator = re.compile(r"{([a-z_]+?)}")
|
||||
|
||||
__max_format_recursion = 3
|
||||
|
||||
def __validate_and_get_format_variables(self) -> List[str]:
|
||||
"""
|
||||
Returns
|
||||
|
|
@ -36,7 +40,8 @@ class StringFormatterValidator(Validator):
|
|||
if open_bracket_count != close_bracket_count:
|
||||
raise self._validation_exception(
|
||||
"Brackets are reserved for {variable_names} and should contain "
|
||||
"a single open and close bracket."
|
||||
"a single open and close bracket.",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
format_variables: List[str] = list(
|
||||
|
|
@ -46,13 +51,15 @@ class StringFormatterValidator(Validator):
|
|||
if len(format_variables) != open_bracket_count:
|
||||
raise self._validation_exception(
|
||||
"{variable_names} should only contain "
|
||||
"lowercase letters and underscores with a single open and close bracket."
|
||||
"lowercase letters and underscores with a single open and close bracket.",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
for variable in format_variables:
|
||||
if iskeyword(variable):
|
||||
raise self._validation_exception(
|
||||
f"'{variable}' is a Python keyword and cannot be used as a variable."
|
||||
f"'{variable}' is a Python keyword and cannot be used as a variable.",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
return format_variables
|
||||
|
|
@ -71,6 +78,40 @@ class StringFormatterValidator(Validator):
|
|||
"""
|
||||
return self._value
|
||||
|
||||
@final
|
||||
def apply_formatter(self, variable_dict: Dict[str, str]) -> str:
|
||||
# Ensure the variable names exist within the entry and overrides
|
||||
for variable_name in self.format_variables:
|
||||
if variable_name not in variable_dict:
|
||||
available_fields = ", ".join(sorted(variable_dict.keys()))
|
||||
raise self._validation_exception(
|
||||
f"Format variable '{variable_name}' does not exist. "
|
||||
f"Available variables: {available_fields}",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
# Keep formatting the format string until no format_variables are present
|
||||
formatter = self
|
||||
recursion_depth = 0
|
||||
max_depth = StringFormatterValidator.__max_format_recursion
|
||||
|
||||
while formatter.format_variables and recursion_depth < max_depth:
|
||||
formatter = StringFormatterValidator(
|
||||
name=self._name,
|
||||
value=formatter.format_string.format(**OrderedDict(variable_dict)),
|
||||
)
|
||||
recursion_depth += 1
|
||||
|
||||
if formatter.format_variables:
|
||||
raise self._validation_exception(
|
||||
f"Attempted to format but failed after reaching max recursion depth of "
|
||||
f"{max_depth}. Try to keep variables dependent on only one other variable at max. "
|
||||
f"Unresolved variables: {', '.join(sorted(formatter.format_variables))}",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
return formatter.format_string
|
||||
|
||||
|
||||
class DictFormatterValidator(LiteralDictValidator):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ from typing import final
|
|||
|
||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||
|
||||
V = TypeVar("V", bound=ValidationException)
|
||||
|
||||
|
||||
class Validator(ABC):
|
||||
"""
|
||||
|
|
@ -38,7 +40,9 @@ class Validator(ABC):
|
|||
error_message=f"should be of type {expected_value_type_name}."
|
||||
)
|
||||
|
||||
def _validation_exception(self, error_message: str) -> ValidationException:
|
||||
def _validation_exception(
|
||||
self, error_message: str, exception_class: Type[V] = ValidationException
|
||||
) -> V:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -50,7 +54,7 @@ class Validator(ABC):
|
|||
Validation exception with a consistent prefix.
|
||||
"""
|
||||
prefix = f"Validation error in {self._name}: "
|
||||
return ValidationException(f"{prefix}{error_message}")
|
||||
return exception_class(f"{prefix}{error_message}")
|
||||
|
||||
|
||||
class BoolValidator(Validator):
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
class ValidationException(ValueError):
|
||||
"""Any user-caused configuration error should result in this error"""
|
||||
|
||||
|
||||
class StringFormattingException(ValidationException):
|
||||
"""Tried to format a string but failed due to user misconfigured variables"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue