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 import pytest
from ytdl_subscribe.entries.entry import Entry from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
@pytest.fixture @pytest.fixture
@ -110,7 +113,12 @@ def validate_entry_dict_contains_valid_formatters():
expected_string = f"test {value} formatting works" expected_string = f"test {value} formatting works"
format_string = f"test {{{key}}} 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 return True

View file

@ -2,6 +2,13 @@ import tempfile
import pytest 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): class TestEntry(object):
def test_entry_properties( def test_entry_properties(
@ -30,13 +37,17 @@ class TestEntry(object):
relative_directory.cleanup() relative_directory.cleanup()
def test_entry_formatter_single_field(self, mock_entry): 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" expected_string = f"prefix {mock_entry.uid} suffix"
assert mock_entry.apply_formatter(format_string) == expected_string assert mock_entry.apply_formatter(format_string) == expected_string
def test_entry_formatter_duplicate_fields(self, mock_entry): 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 = ( expected_string = (
f"prefix {mock_entry.upload_year} {mock_entry.upload_year} suffix" 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): def test_entry_formatter_override(self, mock_entry):
new_uid = "my very own uid" 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" expected_string = f"prefix {new_uid} suffix"
assert ( assert (
@ -64,7 +77,9 @@ class TestEntry(object):
mock_entry.kwargs(key) mock_entry.kwargs(key)
def test_entry_formatter_fails_missing_field(self, mock_entry): 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())) 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"Format variable 'bah_humbug' does not exist for Entry. Available fields: {available_fields}"

View file

@ -1,6 +1,9 @@
import pytest 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, StringFormatterValidator,
) )
from ytdl_subscribe.validators.exceptions import ValidationException 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): def test_parse(self):
format_string = "Here is my {var_one} and {var_two} 💩" format_string = "Here is my {var_one} and {var_two} 💩"
assert StringFormatterValidator( validator = StringFormatterValidator(
name="test_format_variables", value=format_string 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 💩" format_string = "No vars 💩"
assert ( assert (
StringFormatterValidator( StringFormatterValidator(
@ -47,7 +53,7 @@ class TestEntryFormatter(object):
"Try }var_one} and {var_one}", "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 self, format_string, error_message_unequal_brackets_str
): ):
expected_error_msg = ( expected_error_msg = (
@ -60,15 +66,15 @@ class TestEntryFormatter(object):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"format_string", "format_string",
[ [
"Try {var1} no numbers", "Try {var1} numbers",
"Try {VAR1} no caps", "Try {VAR1} caps w/numbers",
"Try {internal{bracket}}", "Try {internal{bracket}}",
"Try }backwards{ facing", "Try }backwards{ facing",
"Try {var_1}}{", "Try {var_1}}{",
"Try {} empty", "Try {} empty",
], ],
) )
def test_parse_fail_variable( def test_validate_fail_bad_variable(
self, format_string, error_message_unequal_regex_matches_str self, format_string, error_message_unequal_regex_matches_str
): ):
expected_error_msg = ( expected_error_msg = (
@ -77,3 +83,37 @@ class TestEntryFormatter(object):
with pytest.raises(ValidationException, match=expected_error_msg): with pytest.raises(ValidationException, match=expected_error_msg):
_ = StringFormatterValidator(name="fail", value=format_string) _ = 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 sanitize_filename import sanitize
from ytdl_subscribe.validators.base.string_formatter_validator import ( from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator, StringFormatterValidator,
) )
from ytdl_subscribe.validators.config.overrides.overrides_validator import (
OverridesValidator,
)
class Entry: class Entry:
@ -105,7 +108,9 @@ class Entry:
} }
def apply_formatter( def apply_formatter(
self, format_string: str, overrides: Optional[Dict] = None self,
formatter: StringFormatterValidator,
overrides: Optional[OverridesValidator] = None,
) -> str: ) -> str:
""" """
Perform a string format on the given format string, using the entry's dict for format 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() entry_dict = self.to_dict()
if overrides: 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( for field_name in formatter.format_variables:
"TODO: UPDATE", format_string
).format_variables
for field_name in field_names:
if field_name not in entry_dict: if field_name not in entry_dict:
available_fields = ", ".join(sorted(entry_dict.keys())) available_fields = ", ".join(sorted(entry_dict.keys()))
raise ValueError( raise ValueError(
@ -127,4 +129,4 @@ class Entry:
f"for {self.__class__.__name__}. Available fields: {available_fields}" 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.downloaders.downloader import Downloader
from ytdl_subscribe.entries.entry import Entry 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 ( from ytdl_subscribe.validators.config.config_options.config_options_validator import (
ConfigOptionsValidator, ConfigOptionsValidator,
) )
@ -80,14 +83,31 @@ class Subscription(object):
ytdl_options=self.ytdl_options.dict, 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): def _post_process_tagging(self, entry: Entry):
id3_options = self.metadata_options.id3 id3_options = self.metadata_options.id3
audio_file = music_tag.load_file( audio_file = music_tag.load_file(
entry.file_path(relative_directory=self.working_directory) entry.file_path(relative_directory=self.working_directory)
) )
for tag, tag_formatter in id3_options.tags.dict.items(): for tag, tag_formatter in id3_options.tags.dict.items():
audio_file[tag] = entry.apply_formatter( audio_file[tag] = self._apply_entry_formatter(
format_string=tag_formatter, overrides=self.overrides.dict entry=entry, formatter=tag_formatter
) )
audio_file.save() audio_file.save()
@ -96,24 +116,28 @@ class Subscription(object):
nfo_options = self.metadata_options.nfo nfo_options = self.metadata_options.nfo
for tag, tag_formatter in nfo_options.tags.dict.items(): for tag, tag_formatter in nfo_options.tags.dict.items():
nfo[tag] = entry.apply_formatter( nfo[tag] = self._apply_entry_formatter(entry=entry, formatter=tag_formatter)
format_string=tag_formatter, overrides=self.overrides.dict
)
# 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( xml = dicttoxml.dicttoxml(
obj=nfo, obj=nfo,
root=True, # We assume all NFOs have a root. Maybe we should not? 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, attr_type=False,
) )
nfo_file_name = entry.apply_formatter( nfo_file_name = self._apply_entry_formatter(
format_string=nfo_options.nfo_name.format_string, entry=entry, formatter=nfo_options.nfo_name
overrides=self.overrides.dict,
) )
nfo_file_path = Path(self.output_options.output_directory.value) / Path( output_directory = self._apply_entry_formatter(
nfo_file_name 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: with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml) nfo_file.write(xml)
@ -132,13 +156,12 @@ class Subscription(object):
relative_directory=self.working_directory relative_directory=self.working_directory
) )
output_directory = entry.apply_formatter( output_directory = self._apply_entry_formatter(
format_string=self.output_options.output_directory.format_string, entry=entry, formatter=self.output_options.output_directory
overrides=self.overrides.dict,
) )
output_file_name = entry.apply_formatter(
format_string=self.output_options.file_name.format_string, output_file_name = self._apply_entry_formatter(
overrides=self.overrides.dict, entry=entry, formatter=self.output_options.file_name
) )
entry_destination_file_path = Path(output_directory) / Path(output_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 relative_directory=self.working_directory
) )
output_thumbnail_name = entry.apply_formatter( output_thumbnail_name = self._apply_entry_formatter(
format_string=self.output_options.thumbnail_name.format_string, entry=entry, formatter=self.output_options.thumbnail_name
overrides=self.overrides.dict,
) )
output_thumbnail_path = Path(output_directory) / Path(output_thumbnail_name) output_thumbnail_path = Path(output_directory) / Path(output_thumbnail_name)

View file

@ -1,5 +1,6 @@
import re import re
from keyword import iskeyword from keyword import iskeyword
from typing import Dict
from typing import List from typing import List
from typing import final from typing import final
@ -80,4 +81,19 @@ class DictFormatterValidator(LiteralDictValidator):
super().__init__(name, value) super().__init__(name, value)
for key in self._keys: 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 abc import ABC
from typing import Any from typing import Any
from typing import Dict from typing import Dict
@ -25,7 +26,9 @@ class Validator(ABC):
def __init__(self, name: str, value: Any): def __init__(self, name: str, value: Any):
self._name = name 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): if not isinstance(self._value, self._expected_value_type):
expected_value_type_name = self._expected_value_type_name or str( 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.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validator import ( from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator, DictFormatterValidator,
) )
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator 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.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validator import ( from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator, DictFormatterValidator,
) )
from ytdl_subscribe.validators.base.string_formatter_validator import ( from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator, StringFormatterValidator,
) )

View file

@ -1,5 +1,5 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator 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, StringFormatterValidator,
) )
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator

View file

@ -1,8 +1,11 @@
import sanitize_filename import sanitize_filename
from ytdl_subscribe.validators.base.string_formatter_validator import ( from ytdl_subscribe.validators.base.string_formatter_validators import (
DictFormatterValidator, DictFormatterValidator,
) )
from ytdl_subscribe.validators.base.string_formatter_validators import (
StringFormatterValidator,
)
class OverridesValidator(DictFormatterValidator): class OverridesValidator(DictFormatterValidator):
@ -11,6 +14,14 @@ class OverridesValidator(DictFormatterValidator):
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
for key in self._keys: for key in self._keys:
self._value[f"sanitized_{key}"] = sanitize_filename.sanitize( sanitized_key_name = f"sanitized_{key}"
self._value[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],
) )