good-bye script builder

This commit is contained in:
Jesse Bannon 2023-12-11 23:52:15 -08:00
parent 5d1ff812f0
commit 5e85d23217
4 changed files with 57 additions and 219 deletions

View file

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

View file

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

View file

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

View file

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