diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index 157a1128..68d8d080 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -1,10 +1,12 @@ from typing import Dict +from typing import List from typing import Optional from ytdl_sub.script.parser import parse from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.utils.exceptions import CycleDetected class Script: @@ -26,6 +28,32 @@ class Script: """ return function_key[1:] + def _traverse_custom_function_dependencies( + self, + custom_function_name: str, + custom_function_dependency: SyntaxTree, + deps: List[str], + ) -> None: + for dep in custom_function_dependency.custom_functions: + if custom_function_name in deps + [dep.name]: + cycle_deps = [custom_function_name] + deps + [dep.name] + cycle_deps_str = " -> ".join([f"%{name}" for name in cycle_deps]) + raise CycleDetected(f"Custom functions contain a cycle: {cycle_deps_str}") + + self._traverse_custom_function_dependencies( + custom_function_name=custom_function_name, + custom_function_dependency=self._functions[dep.name], + deps=deps + [dep.name], + ) + + def _ensure_no_custom_function_cycles(self): + for custom_function_name, custom_function in self._functions.items(): + self._traverse_custom_function_dependencies( + custom_function_name=custom_function_name, + custom_function_dependency=custom_function, + deps=[], + ) + def __init__(self, overrides: Dict[str, str]): self._functions: Dict[str, SyntaxTree] = { # custom_function_name must be passed to properly type custom function @@ -43,6 +71,8 @@ class Script: if not self._is_function(override_name) } + self._ensure_no_custom_function_cycles() + def resolve( self, pre_resolved_variables: Optional[Dict[Variable, Resolvable]] = None ) -> Dict[str, Resolvable]: @@ -56,8 +86,32 @@ class Script: ------- Dict of resolved values """ - return SyntaxTree.resolve_overrides( - parsed_overrides=self._variables, - custom_functions=self._functions, - pre_resolved_variables=pre_resolved_variables, + overrides: Dict[Variable, SyntaxTree] = { + Variable(name): ast for name, ast in self._variables.items() + } + + unresolved_variables: List[Variable] = list(overrides.keys()) + resolved_variables: Dict[Variable, Resolvable] = ( + pre_resolved_variables if pre_resolved_variables else {} ) + + while unresolved_variables: + unresolved_count: int = len(unresolved_variables) + + for variable in unresolved_variables: + if not overrides[variable].has_variable_dependency( + resolved_variables=resolved_variables + ): + resolved_variables[variable] = overrides[variable].resolve( + resolved_variables=resolved_variables, + custom_functions=self._functions, + ) + unresolved_variables.remove(variable) + + if len(unresolved_variables) == unresolved_count: + raise CycleDetected( + f"Cycle detected within these variables: " + f"{', '.join(sorted([var.name for var in unresolved_variables]))}" + ) + + return {variable.name: resolvable for variable, resolvable in resolved_variables.items()} diff --git a/src/ytdl_sub/script/types/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py index b49bd6cf..614df80c 100644 --- a/src/ytdl_sub/script/types/syntax_tree.py +++ b/src/ytdl_sub/script/types/syntax_tree.py @@ -1,17 +1,12 @@ -import copy from dataclasses import dataclass from typing import Dict from typing import List -from typing import Optional from ytdl_sub.script.types.resolvable import ArgumentType -from ytdl_sub.script.types.resolvable import NamedCustomFunction from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable_dependency import VariableDependency -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.utils.exceptions import StringFormattingException @dataclass(frozen=True) @@ -25,7 +20,7 @@ class SyntaxTree(VariableDependency): def resolve( self, resolved_variables: Dict[Variable, Resolvable], - custom_functions: Dict[str, "VariableDependency"], + custom_functions: Dict[str, VariableDependency], ) -> Resolvable: resolved: List[Resolvable] = [] for token in self.ast: @@ -43,73 +38,3 @@ class SyntaxTree(VariableDependency): # Otherwise, to concat multiple resolved outputs, we must concat as strings return String("".join([str(res) for res in resolved])) - - @classmethod - def _traverse_custom_function_dependencies( - cls, - custom_function_name: str, - custom_function_dependency: "SyntaxTree", - custom_functions: Dict[str, "SyntaxTree"], - deps: List[str], - ) -> None: - for dep in custom_function_dependency.custom_functions: - if custom_function_name in deps + [dep.name]: - cycle_deps = [custom_function_name] + deps + [dep.name] - cycle_deps_str = " -> ".join([f"%{name}" for name in cycle_deps]) - raise CycleDetected(f"Custom functions contain a cycle: {cycle_deps_str}") - - cls._traverse_custom_function_dependencies( - custom_function_name=custom_function_name, - custom_function_dependency=custom_functions[dep.name], - custom_functions=custom_functions, - deps=deps + [dep.name], - ) - - @classmethod - def _ensure_no_custom_function_cycles(cls, custom_functions: Dict[str, "SyntaxTree"]): - for custom_function_name, custom_function in custom_functions.items(): - cls._traverse_custom_function_dependencies( - custom_function_name=custom_function_name, - custom_function_dependency=custom_function, - custom_functions=custom_functions, - deps=[], - ) - - @classmethod - def resolve_overrides( - cls, - parsed_overrides: Dict[str, "SyntaxTree"], - custom_functions: Dict[str, "SyntaxTree"], - pre_resolved_variables: Optional[Dict[Variable, Resolvable]], - ) -> Dict[str, Resolvable]: - overrides: Dict[Variable, "SyntaxTree"] = { - Variable(name): ast for name, ast in parsed_overrides.items() - } - - unresolved_variables: List[Variable] = list(overrides.keys()) - resolved_variables: Dict[Variable, Resolvable] = ( - pre_resolved_variables if pre_resolved_variables else {} - ) - - cls._ensure_no_custom_function_cycles(custom_functions=custom_functions) - - while unresolved_variables: - unresolved_count: int = len(unresolved_variables) - - for variable in unresolved_variables: - if not overrides[variable].has_variable_dependency( - resolved_variables=resolved_variables - ): - resolved_variables[variable] = overrides[variable].resolve( - resolved_variables=resolved_variables, - custom_functions=custom_functions, - ) - unresolved_variables.remove(variable) - - if len(unresolved_variables) == unresolved_count: - raise StringFormattingException( - f"Cycle detected within these variables: " - f"{', '.join(sorted([var.name for var in unresolved_variables]))}" - ) - - return {variable.name: resolvable for variable, resolvable in resolved_variables.items()}