good-bye script builder
This commit is contained in:
parent
5d1ff812f0
commit
5e85d23217
4 changed files with 57 additions and 219 deletions
|
|
@ -180,7 +180,7 @@ class Preset(_PresetShell):
|
|||
|
||||
return added_variables
|
||||
|
||||
def __validate_and_get_plugins(self) -> PresetPlugins:
|
||||
def _validate_and_get_plugins(self) -> PresetPlugins:
|
||||
preset_plugins = PresetPlugins()
|
||||
|
||||
for key in self._keys:
|
||||
|
|
@ -194,7 +194,7 @@ class Preset(_PresetShell):
|
|||
|
||||
return preset_plugins
|
||||
|
||||
def _validate_added_variables(self) -> Script:
|
||||
def _validate_variable_usage(self) -> None:
|
||||
"""
|
||||
Validate variables resolve as plugins are executed, and return
|
||||
a mock script which contains actualized added variables from the plugins
|
||||
|
|
@ -216,77 +216,100 @@ class Preset(_PresetShell):
|
|||
added_variables = plugin_options.added_source_variables(
|
||||
unresolved_variables=unresolved_variables
|
||||
).get(PluginOperation.MODIFY_ENTRY_METADATA, set())
|
||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||
unresolved_variables -= added_variables
|
||||
|
||||
if added_variables:
|
||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||
unresolved_variables -= added_variables
|
||||
|
||||
_ = script.resolve(unresolvable=unresolved_variables, update=True)
|
||||
for _, plugin_options in sorted(
|
||||
self.plugins.zipped(), key=lambda pl: pl[0].priority.modify_entry
|
||||
):
|
||||
added_variables = plugin_options.added_source_variables(
|
||||
unresolved_variables=unresolved_variables
|
||||
).get(PluginOperation.MODIFY_ENTRY, set())
|
||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||
unresolved_variables -= added_variables
|
||||
|
||||
if added_variables:
|
||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||
unresolved_variables -= added_variables
|
||||
|
||||
_ = script.resolve(unresolvable=unresolved_variables, update=True)
|
||||
|
||||
# Validate that any formatter in the plugin options can resolve
|
||||
self._validate_formatters(
|
||||
mock_script=script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
validator=plugin_options,
|
||||
)
|
||||
|
||||
self._validate_formatters(
|
||||
mock_script=script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
validator=self.output_options,
|
||||
)
|
||||
|
||||
assert not unresolved_variables
|
||||
_ = script.resolve(update=True)
|
||||
|
||||
return script
|
||||
|
||||
@functools.cache
|
||||
def _get_unresolvable_variables(
|
||||
self,
|
||||
formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
|
||||
) -> Optional[Set[str]]:
|
||||
unresolvable = (
|
||||
self._added_variables.union([VARIABLES.entry_metadata.variable_name])
|
||||
if isinstance(formatter_validator, OverridesStringFormatterValidator)
|
||||
else None
|
||||
)
|
||||
return unresolvable
|
||||
|
||||
def __validate_override_string_formatter_validator(
|
||||
def _validate_string_formatter_validator(
|
||||
self,
|
||||
mock_script: Script,
|
||||
unresolved_variables: Set[str],
|
||||
formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
|
||||
) -> None:
|
||||
try:
|
||||
unresolvable = unresolved_variables
|
||||
if isinstance(formatter_validator, OverridesStringFormatterValidator):
|
||||
unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
|
||||
|
||||
mock_script.resolve_once(
|
||||
{"tmp_var": formatter_validator.format_string},
|
||||
unresolvable=self._get_unresolvable_variables(formatter_validator),
|
||||
unresolvable=unresolvable,
|
||||
)
|
||||
except VariableDoesNotExist as exc:
|
||||
raise StringFormattingVariableNotFoundException(exc) from exc
|
||||
|
||||
def __recursive_preset_validate(
|
||||
def _validate_formatters(
|
||||
self,
|
||||
mock_script: Script,
|
||||
validator: Optional[Validator] = None,
|
||||
unresolved_variables: Set[str],
|
||||
validator: Validator,
|
||||
) -> None:
|
||||
"""
|
||||
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
|
||||
and resolve.
|
||||
"""
|
||||
if validator is None:
|
||||
validator = self
|
||||
|
||||
if isinstance(validator, DictValidator):
|
||||
# pylint: disable=protected-access
|
||||
# Usage of protected variables in other validators is fine. The reason to keep
|
||||
# them protected is for readability when using them in subscriptions.
|
||||
for validator_value in validator._validator_dict.values():
|
||||
self.__recursive_preset_validate(mock_script=mock_script, validator=validator_value)
|
||||
self._validate_formatters(
|
||||
mock_script=mock_script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
validator=validator_value,
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
elif isinstance(validator, ListValidator):
|
||||
for list_value in validator.list:
|
||||
self.__recursive_preset_validate(mock_script=mock_script, validator=list_value)
|
||||
self._validate_formatters(
|
||||
mock_script=mock_script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
validator=list_value,
|
||||
)
|
||||
elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
|
||||
self.__validate_override_string_formatter_validator(
|
||||
mock_script=mock_script, formatter_validator=validator
|
||||
self._validate_string_formatter_validator(
|
||||
mock_script=mock_script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
formatter_validator=validator,
|
||||
)
|
||||
elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
|
||||
for validator_value in validator.dict.values():
|
||||
self.__validate_override_string_formatter_validator(
|
||||
mock_script=mock_script, formatter_validator=validator_value
|
||||
self._validate_string_formatter_validator(
|
||||
mock_script=mock_script,
|
||||
unresolved_variables=unresolved_variables,
|
||||
formatter_validator=validator_value,
|
||||
)
|
||||
|
||||
def _get_presets_to_merge(
|
||||
|
|
@ -364,7 +387,7 @@ class Preset(_PresetShell):
|
|||
key="ytdl_options", validator=YTDLOptions, default={}
|
||||
)
|
||||
|
||||
self.plugins: PresetPlugins = self.__validate_and_get_plugins()
|
||||
self.plugins: PresetPlugins = self._validate_and_get_plugins()
|
||||
self.overrides = self._validate_key(
|
||||
key="overrides", validator=Overrides, default={}
|
||||
).initialize_script(
|
||||
|
|
@ -374,11 +397,7 @@ class Preset(_PresetShell):
|
|||
}
|
||||
)
|
||||
|
||||
mock_script = self._validate_added_variables()
|
||||
|
||||
# After all options are initialized, perform a recursive post-validate that requires
|
||||
# values from multiple validators
|
||||
self.__recursive_preset_validate(mock_script=mock_script)
|
||||
self._validate_variable_usage()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import copy
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.preset_options import OptionsValidator
|
||||
from ytdl_sub.config.preset_options import PluginOperation
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.script import ScriptBuilder
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
|
|
|
|||
|
|
@ -402,126 +402,3 @@ class Script:
|
|||
return resolvable
|
||||
|
||||
raise RuntimeException(f"Tried to get unresolved variable {variable_name}")
|
||||
|
||||
|
||||
class ScriptBuilder:
|
||||
"""
|
||||
Takes a dictionary of both
|
||||
``{ variable_names: syntax }``
|
||||
and
|
||||
``{ %custom_function: syntax }``
|
||||
"""
|
||||
|
||||
def __init__(self, script: Dict[str, str]):
|
||||
self._functions: Dict[str, SyntaxTree] = {
|
||||
# custom_function_name must be passed to properly type custom function
|
||||
# arguments uniquely if they're nested (i.e. $0 to $custom_func___0)
|
||||
_function_name(function_key): parse(
|
||||
text=function_value,
|
||||
name=_function_name(function_key),
|
||||
)
|
||||
for function_key, function_value in script.items()
|
||||
if _is_function(function_key)
|
||||
}
|
||||
|
||||
self._variables: Dict[str, SyntaxTree] = {
|
||||
variable_key: parse(
|
||||
text=variable_value,
|
||||
name=variable_key,
|
||||
)
|
||||
for variable_key, variable_value in script.items()
|
||||
if not _is_function(variable_key)
|
||||
}
|
||||
|
||||
def add(self, variables: Dict[str, str]) -> "ScriptBuilder":
|
||||
for variable_name, variable_definition in variables.items():
|
||||
self._variables[variable_name] = parse(
|
||||
text=variable_definition,
|
||||
name=variable_name,
|
||||
)
|
||||
return self
|
||||
|
||||
def add_resolved(self, variables: Dict[str, Resolvable]) -> "ScriptBuilder":
|
||||
for variable_name, resolvable in variables.items():
|
||||
self._variables[variable_name] = SyntaxTree(ast=[resolvable])
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def _missing_metadata(self) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]:
|
||||
variables_missing_metadata: Dict[str, Set[str]] = defaultdict(set)
|
||||
functions_missing_metadata: Dict[str, Set[str]] = defaultdict(set)
|
||||
|
||||
defined_variables: Set[str] = set(self._variables.keys())
|
||||
defined_functions: Set[str] = set(self._functions.keys())
|
||||
|
||||
while True:
|
||||
variables_missing_metadata_snapshot = copy.deepcopy(variables_missing_metadata)
|
||||
functions_missing_metadata_snapshot = copy.deepcopy(functions_missing_metadata)
|
||||
|
||||
for name, variable in self._variables.items():
|
||||
if diff := {var.name for var in variable.variables}.difference(defined_variables):
|
||||
variables_missing_metadata[name].update(diff)
|
||||
|
||||
if diff := {fun.name for fun in variable.custom_functions}.difference(
|
||||
defined_functions
|
||||
):
|
||||
variables_missing_metadata[name].update(diff)
|
||||
|
||||
for name, function in self._functions.items():
|
||||
if diff := {var.name for var in function.variables}.difference(defined_variables):
|
||||
functions_missing_metadata[name].update(diff)
|
||||
|
||||
if diff := {fun.name for fun in function.custom_functions}.difference(
|
||||
defined_functions
|
||||
):
|
||||
functions_missing_metadata[name].update(diff)
|
||||
|
||||
if (
|
||||
variables_missing_metadata == variables_missing_metadata_snapshot
|
||||
and functions_missing_metadata == functions_missing_metadata_snapshot
|
||||
):
|
||||
break
|
||||
|
||||
defined_variables -= set(variables_missing_metadata.keys())
|
||||
defined_functions -= set(functions_missing_metadata.keys())
|
||||
|
||||
return variables_missing_metadata, functions_missing_metadata
|
||||
|
||||
@classmethod
|
||||
def _build(cls, variables: Dict[str, SyntaxTree], functions: Dict[str, SyntaxTree]) -> Script:
|
||||
script = Script({})
|
||||
script._variables = variables
|
||||
script._functions = functions
|
||||
script._validate()
|
||||
return script
|
||||
|
||||
def partial_build(self) -> Script:
|
||||
missing_variables, missing_functions = self._missing_metadata
|
||||
maybe_resolvable_variables: Dict[str, SyntaxTree] = {
|
||||
name: variable
|
||||
for name, variable in self._variables.items()
|
||||
if name not in missing_variables
|
||||
}
|
||||
maybe_resolvable_functions: Dict[str, SyntaxTree] = {
|
||||
name: function
|
||||
for name, function in self._functions.items()
|
||||
if name not in missing_functions
|
||||
}
|
||||
|
||||
script = self._build(
|
||||
variables=maybe_resolvable_variables, functions=maybe_resolvable_functions
|
||||
)
|
||||
# Update internal variables with anything that is resolved
|
||||
for variable_name, variable_output in script.resolve(update=True).output.items():
|
||||
self._variables[variable_name] = SyntaxTree([variable_output])
|
||||
return script
|
||||
|
||||
def build(self) -> Script:
|
||||
for missing_metadata in self._missing_metadata:
|
||||
for name, missing in missing_metadata.items():
|
||||
raise ScriptBuilderMissingDefinitions(
|
||||
f"{name} is missing the following definitions: {', '.join(missing)}"
|
||||
)
|
||||
|
||||
return self._build(variables=self._variables, functions=self._functions)
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.script import ScriptBuilder
|
||||
from ytdl_sub.script.types.map import Map
|
||||
from ytdl_sub.script.types.resolvable import String
|
||||
from ytdl_sub.script.utils.exceptions import ScriptBuilderMissingDefinitions
|
||||
from ytdl_sub.script.utils.exceptions import VariableDoesNotExist
|
||||
|
||||
|
||||
class TestScriptBuilder:
|
||||
def test_partial_update_script(self):
|
||||
# to be resolved later
|
||||
entry_map = Map({String("title"): String("the title")})
|
||||
|
||||
script = ScriptBuilder(
|
||||
{
|
||||
"entry": "{ {} }",
|
||||
"title": "{%map_get(entry, 'title', '')}",
|
||||
"resolved_override": "{override} mom",
|
||||
}
|
||||
)
|
||||
|
||||
assert script.partial_build().resolve(unresolvable={"entry"})
|
||||
|
||||
with pytest.raises(
|
||||
ScriptBuilderMissingDefinitions,
|
||||
match=re.escape("resolved_override is missing the following definitions: override"),
|
||||
):
|
||||
script.build()
|
||||
|
||||
script.add({"override": "hi"})
|
||||
script.add_resolved({"entry": entry_map})
|
||||
|
||||
script.build()
|
||||
|
||||
# script.resolve(unresolvable={"entry"}, update=True)
|
||||
# assert script.get("override") == String("hi")
|
||||
# assert script.get("resolved_override") == String("hi mom")
|
||||
#
|
||||
# script.add(
|
||||
# {
|
||||
# "new_variable_titlecase": "{%titlecase(new_variable_upper)}",
|
||||
# "new_variable": "{resolved_override} {title}",
|
||||
# "new_variable_upper": "{%upper(new_variable)}",
|
||||
# }
|
||||
# ).resolve(resolved={"entry": entry_map}, update=True)
|
||||
#
|
||||
# assert script.get("title") == String("the title")
|
||||
# assert script.get("new_variable") == String("hi mom the title")
|
||||
# assert script.get("new_variable_upper") == String("HI MOM THE TITLE")
|
||||
# assert script.get("new_variable_titlecase") == String("Hi Mom The Title")
|
||||
# assert script.get("entry") == entry_map
|
||||
Loading…
Reference in a new issue