246 lines
9.3 KiB
Python
246 lines
9.3 KiB
Python
from typing import Any, Dict, Iterable, Optional, Set, Type, TypeVar
|
|
|
|
from ytdl_sub.entries.entry import Entry
|
|
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
|
from ytdl_sub.entries.variables.override_variables import (
|
|
REQUIRED_OVERRIDE_VARIABLE_NAMES,
|
|
OverrideHelpers,
|
|
)
|
|
from ytdl_sub.script.parser import parse
|
|
from ytdl_sub.script.script import Script
|
|
from ytdl_sub.script.types.function import BuiltInFunction
|
|
from ytdl_sub.script.types.resolvable import Resolvable, String
|
|
from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
|
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
|
from ytdl_sub.utils.exceptions import (
|
|
InvalidVariableNameException,
|
|
StringFormattingException,
|
|
ValidationException,
|
|
)
|
|
from ytdl_sub.utils.script import ScriptUtils
|
|
from ytdl_sub.utils.scriptable import Scriptable
|
|
from ytdl_sub.validators.string_formatter_validators import (
|
|
StringFormatterValidator,
|
|
UnstructuredDictFormatterValidator,
|
|
)
|
|
|
|
ExpectedT = TypeVar("ExpectedT")
|
|
|
|
|
|
class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|
"""
|
|
Allows you to define variables that can be used in any EntryFormatter or OverridesFormatter.
|
|
|
|
:Usage:
|
|
|
|
.. code-block:: yaml
|
|
|
|
presets:
|
|
my_example_preset:
|
|
overrides:
|
|
output_directory: "/path/to/media"
|
|
custom_file_name: "{upload_date_standardized}.{title_sanitized}"
|
|
|
|
# Then use the override variables in the output options
|
|
output_options:
|
|
output_directory: "{output_directory}"
|
|
file_name: "{custom_file_name}.{ext}"
|
|
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
|
|
|
|
Override variables can contain explicit values and other variables, including both override
|
|
and source variables.
|
|
|
|
In addition, any override variable defined will automatically create a ``sanitized`` variable
|
|
for use. In the example above, ``output_directory_sanitized`` will exist and perform
|
|
sanitization on the value when used.
|
|
"""
|
|
|
|
@classmethod
|
|
def partial_validate(cls, name: str, value: Any) -> None:
|
|
dict_formatter = UnstructuredDictFormatterValidator(name=name, value=value)
|
|
_ = [parse(format_string) for format_string in dict_formatter.dict_with_format_strings]
|
|
|
|
def __init__(self, name, value):
|
|
UnstructuredDictFormatterValidator.__init__(self, name, value)
|
|
Scriptable.__init__(self, initialize_base_script=True)
|
|
|
|
for key in self._keys:
|
|
self.ensure_variable_name_valid(key)
|
|
|
|
self.unresolvable.add(VARIABLES.entry_metadata.variable_name)
|
|
self.unresolvable.update(REQUIRED_OVERRIDE_VARIABLE_NAMES)
|
|
|
|
def ensure_added_plugin_variable_valid(self, added_variable: str) -> bool:
|
|
"""
|
|
Returns False if the variable exists as a non-override.
|
|
|
|
Raises
|
|
------
|
|
ValidationException
|
|
If the variable is already added as an override variable.
|
|
"""
|
|
try:
|
|
self.ensure_variable_name_valid(added_variable)
|
|
except ValidationException:
|
|
return False
|
|
|
|
if added_variable in self.keys:
|
|
raise self._validation_exception(
|
|
f"Override variable with name {added_variable} cannot be used since it is"
|
|
" added by a plugin."
|
|
)
|
|
|
|
return True
|
|
|
|
def ensure_variable_names_not_a_plugin(self, plugin_names: Iterable[str]) -> None:
|
|
"""
|
|
Throws an error if an override variable or function has the same name as a
|
|
preset key. This is to avoid confusion when accidentally defining things in
|
|
overrides that are meant to be in the preset.
|
|
"""
|
|
for name in self.keys:
|
|
if name.startswith("%"):
|
|
name = name[1:]
|
|
|
|
if name in plugin_names:
|
|
raise self._validation_exception(
|
|
f"Override variable with name {name} cannot be used since it is"
|
|
" the name of a plugin. Perhaps you meant to define it as a plugin? If so,"
|
|
" indent it left to make it at the same level as overrides.",
|
|
exception_class=InvalidVariableNameException,
|
|
)
|
|
|
|
def ensure_variable_name_valid(self, name: str) -> None:
|
|
"""
|
|
Ensures the variable name does not collide with any entry variables or built-in functions.
|
|
"""
|
|
if not OverrideHelpers.is_valid_name(name):
|
|
override_type = "function" if name.startswith("%") else "variable"
|
|
raise self._validation_exception(
|
|
f"Override {override_type} with name {name} is invalid. Names must be"
|
|
" lower_snake_cased and begin with a letter.",
|
|
exception_class=InvalidVariableNameException,
|
|
)
|
|
|
|
if OverrideHelpers.is_entry_variable_name(name):
|
|
raise self._validation_exception(
|
|
f"Override variable with name {name} cannot be used since it is a"
|
|
" built-in ytdl-sub entry variable name.",
|
|
exception_class=InvalidVariableNameException,
|
|
)
|
|
|
|
if OverrideHelpers.is_function_name(name):
|
|
raise self._validation_exception(
|
|
f"Override function definition with name {name} cannot be used since it is"
|
|
" a built-in ytdl-sub function name.",
|
|
exception_class=InvalidVariableNameException,
|
|
)
|
|
|
|
def initial_variables(
|
|
self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
|
|
) -> Dict[str, SyntaxTree]:
|
|
"""
|
|
Returns
|
|
-------
|
|
Variables and format strings for all Override variables + additional variables (Optional)
|
|
"""
|
|
initial_variables: Dict[str, SyntaxTree] = self.dict_with_parsed_format_strings
|
|
if unresolved_variables:
|
|
initial_variables |= unresolved_variables
|
|
return ScriptUtils.add_sanitized_parsed_variables(initial_variables)
|
|
|
|
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
|
|
"""
|
|
Initialize the override script with any unresolved variables
|
|
"""
|
|
self.script.add_parsed(
|
|
self.initial_variables(
|
|
unresolved_variables={
|
|
var_name: SyntaxTree(
|
|
ast=[
|
|
BuiltInFunction(
|
|
name="throw",
|
|
args=[
|
|
String(f"Plugin variable {var_name} has not been created yet")
|
|
],
|
|
)
|
|
]
|
|
)
|
|
for var_name in unresolved_variables
|
|
}
|
|
)
|
|
)
|
|
self.unresolvable.update(unresolved_variables)
|
|
self.update_script()
|
|
return self
|
|
|
|
def _apply_to_resolvable(
|
|
self,
|
|
formatter: StringFormatterValidator,
|
|
entry: Optional[Entry],
|
|
function_overrides: Optional[Dict[str, str]],
|
|
) -> Resolvable:
|
|
script: Script = self.script
|
|
unresolvable: Set[str] = self.unresolvable
|
|
if entry:
|
|
script = entry.script
|
|
unresolvable = entry.unresolvable
|
|
|
|
# Update the script internally so long as we are not supplying overrides
|
|
# that could alter the script with one-off state
|
|
update = function_overrides is None
|
|
|
|
try:
|
|
return script.resolve_once(
|
|
dict({"tmp_var": formatter.format_string}, **(function_overrides or {})),
|
|
unresolvable=unresolvable,
|
|
update=update,
|
|
)["tmp_var"]
|
|
except ScriptVariableNotResolved as exc:
|
|
raise StringFormattingException(
|
|
"Tried to resolve the following script, but could not due to unresolved "
|
|
f"variables:\n {formatter.format_string}\n"
|
|
"This is most likely due to circular dependencies in variables. "
|
|
"If you think otherwise, please file a bug on GitHub and post your config. Thanks!"
|
|
) from exc
|
|
|
|
def apply_formatter(
|
|
self,
|
|
formatter: StringFormatterValidator,
|
|
entry: Optional[Entry] = None,
|
|
function_overrides: Optional[Dict[str, str]] = None,
|
|
expected_type: Type[ExpectedT] = str,
|
|
) -> ExpectedT:
|
|
"""
|
|
Parameters
|
|
----------
|
|
formatter
|
|
Formatter to apply
|
|
entry
|
|
Optional. Entry to add source variables to the formatter
|
|
function_overrides
|
|
Optional. Explicit values to override the overrides themselves and source variables
|
|
expected_type
|
|
The expected type that should return. Defaults to string.
|
|
|
|
Returns
|
|
-------
|
|
The format_string after .format has been called
|
|
|
|
Raises
|
|
------
|
|
StringFormattingException
|
|
If the formatter that is trying to be resolved cannot
|
|
"""
|
|
out = formatter.post_process(
|
|
self._apply_to_resolvable(
|
|
formatter=formatter, entry=entry, function_overrides=function_overrides
|
|
).native
|
|
)
|
|
|
|
if not isinstance(out, expected_type):
|
|
raise StringFormattingException(
|
|
f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'"
|
|
)
|
|
|
|
return out
|