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
|
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())
|
||||||
|
|
||||||
|
if added_variables:
|
||||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||||
unresolved_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())
|
||||||
|
|
||||||
|
if added_variables:
|
||||||
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
script.add(ScriptUtils.add_dummy_variables(added_variables))
|
||||||
unresolved_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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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