better recursion functionality

This commit is contained in:
jbannon 2022-04-06 07:16:07 +00:00
parent 5fed8a7b64
commit cbe73adbf7
5 changed files with 66 additions and 45 deletions

View file

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

View file

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

View file

@ -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):
"""

View file

@ -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):

View file

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