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 return added_variables
def __validate_and_get_plugins(self) -> PresetPlugins: def _validate_and_get_plugins(self) -> PresetPlugins:
preset_plugins = PresetPlugins() preset_plugins = PresetPlugins()
for key in self._keys: for key in self._keys:
@ -194,7 +194,7 @@ class Preset(_PresetShell):
return preset_plugins return preset_plugins
def _validate_added_variables(self) -> Script: def _validate_variable_usage(self) -> None:
""" """
Validate variables resolve as plugins are executed, and return Validate variables resolve as plugins are executed, and return
a mock script which contains actualized added variables from the plugins 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( added_variables = plugin_options.added_source_variables(
unresolved_variables=unresolved_variables unresolved_variables=unresolved_variables
).get(PluginOperation.MODIFY_ENTRY_METADATA, set()) ).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( for _, plugin_options in sorted(
self.plugins.zipped(), key=lambda pl: pl[0].priority.modify_entry self.plugins.zipped(), key=lambda pl: pl[0].priority.modify_entry
): ):
added_variables = plugin_options.added_source_variables( added_variables = plugin_options.added_source_variables(
unresolved_variables=unresolved_variables unresolved_variables=unresolved_variables
).get(PluginOperation.MODIFY_ENTRY, set()) ).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 assert not unresolved_variables
_ = script.resolve(update=True)
return script return script
@functools.cache def _validate_string_formatter_validator(
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(
self, self,
mock_script: Script, mock_script: Script,
unresolved_variables: Set[str],
formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
) -> None: ) -> None:
try: try:
unresolvable = unresolved_variables
if isinstance(formatter_validator, OverridesStringFormatterValidator):
unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
mock_script.resolve_once( mock_script.resolve_once(
{"tmp_var": formatter_validator.format_string}, {"tmp_var": formatter_validator.format_string},
unresolvable=self._get_unresolvable_variables(formatter_validator), unresolvable=unresolvable,
) )
except VariableDoesNotExist as exc: except VariableDoesNotExist as exc:
raise StringFormattingVariableNotFoundException(exc) from exc raise StringFormattingVariableNotFoundException(exc) from exc
def __recursive_preset_validate( def _validate_formatters(
self, self,
mock_script: Script, mock_script: Script,
validator: Optional[Validator] = None, unresolved_variables: Set[str],
validator: Validator,
) -> None: ) -> None:
""" """
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
and resolve. and resolve.
""" """
if validator is None:
validator = self
if isinstance(validator, DictValidator): if isinstance(validator, DictValidator):
# pylint: disable=protected-access # pylint: disable=protected-access
# Usage of protected variables in other validators is fine. The reason to keep # Usage of protected variables in other validators is fine. The reason to keep
# them protected is for readability when using them in subscriptions. # them protected is for readability when using them in subscriptions.
for validator_value in validator._validator_dict.values(): 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 # pylint: enable=protected-access
elif isinstance(validator, ListValidator): elif isinstance(validator, ListValidator):
for list_value in validator.list: 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)): elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
self.__validate_override_string_formatter_validator( self._validate_string_formatter_validator(
mock_script=mock_script, formatter_validator=validator mock_script=mock_script,
unresolved_variables=unresolved_variables,
formatter_validator=validator,
) )
elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
for validator_value in validator.dict.values(): for validator_value in validator.dict.values():
self.__validate_override_string_formatter_validator( self._validate_string_formatter_validator(
mock_script=mock_script, formatter_validator=validator_value mock_script=mock_script,
unresolved_variables=unresolved_variables,
formatter_validator=validator_value,
) )
def _get_presets_to_merge( def _get_presets_to_merge(
@ -364,7 +387,7 @@ class Preset(_PresetShell):
key="ytdl_options", validator=YTDLOptions, default={} 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( self.overrides = self._validate_key(
key="overrides", validator=Overrides, default={} key="overrides", validator=Overrides, default={}
).initialize_script( ).initialize_script(
@ -374,11 +397,7 @@ class Preset(_PresetShell):
} }
) )
mock_script = self._validate_added_variables() self._validate_variable_usage()
# After all options are initialized, perform a recursive post-validate that requires
# values from multiple validators
self.__recursive_preset_validate(mock_script=mock_script)
@property @property
def name(self) -> str: def name(self) -> str:

View file

@ -1,14 +1,11 @@
import copy import copy
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from ytdl_sub.config.preset_options import OptionsValidator from ytdl_sub.config.preset_options import OptionsValidator
from ytdl_sub.config.preset_options import PluginOperation 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.strict_dict_validator import StrictDictValidator
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator

View file

@ -402,126 +402,3 @@ class Script:
return resolvable return resolvable
raise RuntimeException(f"Tried to get unresolved variable {variable_name}") 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