diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index 20d53e18..53d3dc02 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -11,6 +11,7 @@ from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import UNREACHABLE from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments +from ytdl_sub.script.utils.exceptions import RuntimeException from ytdl_sub.script.utils.name_validation import validate_variable_name # pylint: disable=missing-raises-doc @@ -143,6 +144,12 @@ class Script: f"{nested_custom_function.num_input_args}" ) + def _validate(self) -> None: + self._ensure_no_custom_function_cycles() + self._ensure_custom_function_arguments_valid() + self._ensure_no_variable_cycles() + self._ensure_custom_function_usage_num_input_arguments_valid() + def __init__(self, script: Dict[str, str]): function_names: Set[str] = { self._function_name(name) for name in script.keys() if self._is_function(name) @@ -174,25 +181,29 @@ class Script: for variable_key, variable_value in script.items() if not self._is_function(variable_key) } + self._validate() - self._ensure_no_custom_function_cycles() - self._ensure_custom_function_arguments_valid() - self._ensure_no_variable_cycles() - self._ensure_custom_function_usage_num_input_arguments_valid() + def _update_internally(self, resolved_variables: Dict[str, Resolvable]) -> None: + for variable_name, resolved in resolved_variables.items(): + self._variables[variable_name] = SyntaxTree(ast=[resolved]) def resolve( self, resolved: Optional[Dict[str, Resolvable]] = None, unresolvable: Optional[Set[str]] = None, + update: bool = False, ) -> Dict[str, Resolvable]: """ Parameters ---------- resolved - Optional variables that have been resolved elsewhere and could be used in this script + Optional. Variables that have been resolved elsewhere and could be used in this script unresolvable - Variables that cannot be resolved, forcing any variable that depends on it to not be - resolved. + Optional. Variables that cannot be resolved, forcing any variable that depends on it + to not be resolved. + update + Optional. Whether to update the internal representation of variables with their + resolved value (if they get resolved). Returns ------- @@ -232,4 +243,32 @@ class Script: # since cycles are detected in __init__ raise UNREACHABLE - return {variable.name: resolvable for variable, resolvable in resolved.items()} + resolved_variables = { + variable.name: resolvable for variable, resolvable in resolved.items() + } + if update: + self._update_internally(resolved_variables=resolved_variables) + + return resolved_variables + + def add(self, variables: Dict[str, str]) -> "Script": + for variable_name, variable_definition in variables.items(): + self._variables[variable_name] = parse( + text=variable_definition, + name=variable_name, + custom_function_names=set(self._functions.keys()), + variable_names=set(self._variables.keys()).union(variables.keys()), + ) + self._validate() + return self + + def get(self, variable_name: str) -> Resolvable: + if variable_name not in self._variables: + raise RuntimeException( + f"Tried to get resolved variable {variable_name}, but it does not exist" + ) + + if (resolvable := self._variables[variable_name].resolvable) is not None: + return resolvable + + raise RuntimeException(f"Tried to get unresolved variable {variable_name}") diff --git a/src/ytdl_sub/script/types/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py index 530eb4c3..d2797246 100644 --- a/src/ytdl_sub/script/types/syntax_tree.py +++ b/src/ytdl_sub/script/types/syntax_tree.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import Dict from typing import List +from typing import Optional from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import Resolvable @@ -38,3 +39,9 @@ class SyntaxTree(VariableDependency): # Otherwise, to concat multiple resolved outputs, we must concat as strings return String("".join([str(res) for res in resolved])) + + @property + def resolvable(self) -> Optional[Resolvable]: + if len(self.ast) == 1 and isinstance(self.ast[0], Resolvable): + return self.ast[0] + return None diff --git a/src/ytdl_sub/script/utils/exceptions.py b/src/ytdl_sub/script/utils/exceptions.py index 762a0a56..7317387d 100644 --- a/src/ytdl_sub/script/utils/exceptions.py +++ b/src/ytdl_sub/script/utils/exceptions.py @@ -7,6 +7,10 @@ class UserException(ValidationException, ABC): """It's the user's fault!""" +class RuntimeException(ValueError, ABC): + """Exception thrown at runtime during resolution""" + + class InvalidSyntaxException(UserException): """Syntax is incorrect""" @@ -43,10 +47,6 @@ class CycleDetected(UserException): """A cycle exists within a user's script""" -class RuntimeException(ValueError, ABC): - """Exception thrown at runtime during resolution""" - - class FunctionRuntimeException(RuntimeException): """Exception thrown when a ytdl-sub function has an error occur at runtime""" diff --git a/tests/unit/script/test_script.py b/tests/unit/script/test_script.py index 5bf47c90..80b2126f 100644 --- a/tests/unit/script/test_script.py +++ b/tests/unit/script/test_script.py @@ -34,16 +34,23 @@ class TestScript: "entry": "{%throw('entry has not been populated yet')}", "title": "{%map_get(entry, 'title')}", "override": "hi", + "resolved_override": "{override} mom", } ) - overrides = script.resolve(unresolvable={"entry"}) - assert overrides == {"override": String("hi")} + script.resolve(unresolvable={"entry"}, update=True) + assert script.get("override") == String("hi") + assert script.get("resolved_override") == String("hi mom") - entry_map = ResolvedMap({String("title"): String("the title")}) - entry_output = script.resolve(resolved=dict(overrides, **{"entry": entry_map})) - assert entry_output == { - "override": String("hi"), - "entry": entry_map, - "title": String("the title"), - } + script.add( + { + "new_variable": "{resolved_override} {title}", + "new_variable_upper": "{%upper(new_variable)}", + } + ).resolve( + resolved={"entry": ResolvedMap({String("title"): String("the title")})}, 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")