return string formatters in overrides dict

This commit is contained in:
jbannon 2022-04-06 06:09:16 +00:00
parent 288e5cb892
commit 451b3564c5
11 changed files with 170 additions and 53 deletions

View file

@ -1,6 +1,9 @@
import pytest
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
@pytest.fixture
@ -110,7 +113,12 @@ def validate_entry_dict_contains_valid_formatters():
expected_string = f"test {value} formatting works"
format_string = f"test {{{key}}} formatting works"
assert entry.apply_formatter(format_string) == expected_string
assert (
entry.apply_formatter(
StringFormatterValidator(name="test", value=format_string)
)
== expected_string
)
return True

View file

@ -2,6 +2,13 @@ import tempfile
import pytest
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
from ytdl_subscribe.validators.config.overrides.overrides_validator import (
OverridesValidator,
)
class TestEntry(object):
def test_entry_properties(
@ -30,13 +37,17 @@ class TestEntry(object):
relative_directory.cleanup()
def test_entry_formatter_single_field(self, mock_entry):
format_string = f"prefix {{uid}} suffix"
format_string = StringFormatterValidator(
name="test", value=f"prefix {{uid}} suffix"
)
expected_string = f"prefix {mock_entry.uid} suffix"
assert mock_entry.apply_formatter(format_string) == expected_string
def test_entry_formatter_duplicate_fields(self, mock_entry):
format_string = f"prefix {{upload_year}} {{upload_year}} suffix"
format_string = StringFormatterValidator(
name="test", value=f"prefix {{upload_year}} {{upload_year}} suffix"
)
expected_string = (
f"prefix {mock_entry.upload_year} {mock_entry.upload_year} suffix"
)
@ -45,9 +56,11 @@ class TestEntry(object):
def test_entry_formatter_override(self, mock_entry):
new_uid = "my very own uid"
overrides = {"uid": new_uid}
overrides = OverridesValidator(name="test", value={"uid": new_uid})
format_string = f"prefix {{uid}} suffix"
format_string = StringFormatterValidator(
name="test", value=f"prefix {{uid}} suffix"
)
expected_string = f"prefix {new_uid} suffix"
assert (
@ -64,7 +77,9 @@ class TestEntry(object):
mock_entry.kwargs(key)
def test_entry_formatter_fails_missing_field(self, mock_entry):
format_string = f"prefix {{bah_humbug}} suffix"
format_string = StringFormatterValidator(
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}"

View file

@ -1,6 +1,9 @@
import pytest
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
from ytdl_subscribe.validators.exceptions import ValidationException
@ -22,14 +25,17 @@ def error_message_unequal_regex_matches_str():
)
class TestEntryFormatter(object):
class TestStringFormatterValidator(object):
def test_parse(self):
format_string = "Here is my {var_one} and {var_two} 💩"
assert StringFormatterValidator(
validator = StringFormatterValidator(
name="test_format_variables", value=format_string
).format_variables == ["var_one", "var_two"]
)
def test_parse_no_variables(self):
assert validator.format_string == format_string
assert validator.format_variables == ["var_one", "var_two"]
def test_format_variables(self):
format_string = "No vars 💩"
assert (
StringFormatterValidator(
@ -47,7 +53,7 @@ class TestEntryFormatter(object):
"Try }var_one} and {var_one}",
],
)
def test_parse_fail_uneven_brackets(
def test_validate_fail_uneven_brackets(
self, format_string, error_message_unequal_brackets_str
):
expected_error_msg = (
@ -60,15 +66,15 @@ class TestEntryFormatter(object):
@pytest.mark.parametrize(
"format_string",
[
"Try {var1} no numbers",
"Try {VAR1} no caps",
"Try {var1} numbers",
"Try {VAR1} caps w/numbers",
"Try {internal{bracket}}",
"Try }backwards{ facing",
"Try {var_1}}{",
"Try {} empty",
],
)
def test_parse_fail_variable(
def test_validate_fail_bad_variable(
self, format_string, error_message_unequal_regex_matches_str
):
expected_error_msg = (
@ -77,3 +83,37 @@ class TestEntryFormatter(object):
with pytest.raises(ValidationException, match=expected_error_msg):
_ = StringFormatterValidator(name="fail", value=format_string)
@pytest.mark.parametrize(
"format_string, bad_variable",
[
("keyword {while}", "while"),
("{try} {valid_var}", "try"),
],
)
def test_validate_fail_variable_keyword_or_not_identifier(
self, format_string, bad_variable
):
expected_error_msg = (
f"Validation error in fail: "
f"'{bad_variable}' is a Python keyword and cannot be used as a variable."
)
with pytest.raises(ValidationException, match=expected_error_msg):
_ = StringFormatterValidator(name="fail", value=format_string)
class TestDictFormatterValidator(object):
def test_validates_values(self):
key1_format_string = "string with {variable}"
key2_format_string = "no variables"
validator = DictFormatterValidator(
name="validator",
value={"key1": key1_format_string, "key2": key2_format_string},
)
assert validator.dict["key1"].format_string == key1_format_string
assert validator.dict["key2"].format_string == key2_format_string
assert validator.dict["key1"].format_variables == ["variable"]
assert validator.dict["key2"].format_variables == []

View file

@ -5,9 +5,12 @@ from typing import Optional
from sanitize_filename import sanitize
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
from ytdl_subscribe.validators.config.overrides.overrides_validator import (
OverridesValidator,
)
class Entry:
@ -105,7 +108,9 @@ class Entry:
}
def apply_formatter(
self, format_string: str, overrides: Optional[Dict] = None
self,
formatter: StringFormatterValidator,
overrides: Optional[OverridesValidator] = None,
) -> str:
"""
Perform a string format on the given format string, using the entry's dict for format
@ -113,13 +118,10 @@ class Entry:
"""
entry_dict = self.to_dict()
if overrides:
entry_dict = dict(entry_dict, **overrides)
# TODO: need to check recursively populate format variables
entry_dict = dict(entry_dict, **overrides.dict_with_format_strings)
field_names = StringFormatterValidator(
"TODO: UPDATE", format_string
).format_variables
for field_name in field_names:
for field_name in formatter.format_variables:
if field_name not in entry_dict:
available_fields = ", ".join(sorted(entry_dict.keys()))
raise ValueError(
@ -127,4 +129,4 @@ class Entry:
f"for {self.__class__.__name__}. Available fields: {available_fields}"
)
return format_string.format(**entry_dict)
return formatter.format_string.format(**entry_dict)

View file

@ -10,6 +10,9 @@ from PIL import Image
from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
from ytdl_subscribe.validators.config.config_options.config_options_validator import (
ConfigOptionsValidator,
)
@ -80,14 +83,31 @@ class Subscription(object):
ytdl_options=self.ytdl_options.dict,
)
def _apply_entry_formatter(
self, entry: Entry, formatter: StringFormatterValidator
) -> str:
"""
Parameters
----------
entry
Entry with values to use in the formatter
formatter
The formatter itself
Returns
-------
The format_string after .format has been called on it using entry and override values
"""
return entry.apply_formatter(formatter=formatter, overrides=self.overrides)
def _post_process_tagging(self, entry: Entry):
id3_options = self.metadata_options.id3
audio_file = music_tag.load_file(
entry.file_path(relative_directory=self.working_directory)
)
for tag, tag_formatter in id3_options.tags.dict.items():
audio_file[tag] = entry.apply_formatter(
format_string=tag_formatter, overrides=self.overrides.dict
audio_file[tag] = self._apply_entry_formatter(
entry=entry, formatter=tag_formatter
)
audio_file.save()
@ -96,24 +116,28 @@ class Subscription(object):
nfo_options = self.metadata_options.nfo
for tag, tag_formatter in nfo_options.tags.dict.items():
nfo[tag] = entry.apply_formatter(
format_string=tag_formatter, overrides=self.overrides.dict
)
nfo[tag] = self._apply_entry_formatter(entry=entry, formatter=tag_formatter)
# Write the nfo tags to XML with the nfo_root
nfo_root = self._apply_entry_formatter(
entry=entry, formatter=nfo_options.nfo_root
)
xml = dicttoxml.dicttoxml(
obj=nfo,
root=True, # We assume all NFOs have a root. Maybe we should not?
custom_root=nfo_options.nfo_root.value,
custom_root=nfo_root,
attr_type=False,
)
nfo_file_name = entry.apply_formatter(
format_string=nfo_options.nfo_name.format_string,
overrides=self.overrides.dict,
nfo_file_name = self._apply_entry_formatter(
entry=entry, formatter=nfo_options.nfo_name
)
nfo_file_path = Path(self.output_options.output_directory.value) / Path(
nfo_file_name
output_directory = self._apply_entry_formatter(
entry=entry, formatter=self.output_options.output_directory
)
# Save the nfo's XML to file
nfo_file_path = Path(output_directory) / Path(nfo_file_name)
with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml)
@ -132,13 +156,12 @@ class Subscription(object):
relative_directory=self.working_directory
)
output_directory = entry.apply_formatter(
format_string=self.output_options.output_directory.format_string,
overrides=self.overrides.dict,
output_directory = self._apply_entry_formatter(
entry=entry, formatter=self.output_options.output_directory
)
output_file_name = entry.apply_formatter(
format_string=self.output_options.file_name.format_string,
overrides=self.overrides.dict,
output_file_name = self._apply_entry_formatter(
entry=entry, formatter=self.output_options.file_name
)
entry_destination_file_path = Path(output_directory) / Path(output_file_name)
@ -151,9 +174,8 @@ class Subscription(object):
relative_directory=self.working_directory
)
output_thumbnail_name = entry.apply_formatter(
format_string=self.output_options.thumbnail_name.format_string,
overrides=self.overrides.dict,
output_thumbnail_name = self._apply_entry_formatter(
entry=entry, formatter=self.output_options.thumbnail_name
)
output_thumbnail_path = Path(output_directory) / Path(output_thumbnail_name)

View file

@ -1,5 +1,6 @@
import re
from keyword import iskeyword
from typing import Dict
from typing import List
from typing import final
@ -80,4 +81,19 @@ class DictFormatterValidator(LiteralDictValidator):
super().__init__(name, value)
for key in self._keys:
_ = self._validate_key(key=key, validator=StringFormatterValidator)
self._value[key] = self._validate_key(
key=key, validator=StringFormatterValidator
)
@property
def dict(self) -> Dict[str, StringFormatterValidator]:
"""Returns dict with string formatter values"""
return self._value
@property
def dict_with_format_strings(self) -> Dict[str, str]:
"""Returns dict with the format strings themselves"""
return {
key: string_formatter.format_string
for key, string_formatter in self.dict.items()
}

View file

@ -1,3 +1,4 @@
import copy
from abc import ABC
from typing import Any
from typing import Dict
@ -25,7 +26,9 @@ class Validator(ABC):
def __init__(self, name: str, value: Any):
self._name = name
self._value = value
self._value = copy.deepcopy(
value
) # Always deep copy to avoid editing references
if not isinstance(self._value, self._expected_value_type):
expected_value_type_name = self._expected_value_type_name or str(

View file

@ -1,5 +1,5 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator,
)
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator

View file

@ -1,8 +1,8 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)

View file

@ -1,5 +1,5 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator

View file

@ -1,8 +1,11 @@
import sanitize_filename
from ytdl_subscribe.validators.base.string_formatter_validator import (
from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
class OverridesValidator(DictFormatterValidator):
@ -11,6 +14,14 @@ class OverridesValidator(DictFormatterValidator):
def __init__(self, name, value):
super().__init__(name, value)
for key in self._keys:
self._value[f"sanitized_{key}"] = sanitize_filename.sanitize(
self._value[key]
sanitized_key_name = f"sanitized_{key}"
# First, sanitize the format string
self._value[sanitized_key_name] = sanitize_filename.sanitize(
self._value[key].format_string
)
# Then, convert it into a StringFormatterValidator
self._value[sanitized_key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=self._value[sanitized_key_name],
)