diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index ac864a99..c151f984 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -181,7 +181,7 @@ class Script: self._ensure_custom_function_usage_num_input_arguments_valid() def resolve( - self, pre_resolved_variables: Optional[Dict[Variable, Resolvable]] = None + self, pre_resolved_variables: Optional[Dict[str, Resolvable]] = None ) -> Dict[str, Resolvable]: """ Parameters @@ -197,10 +197,10 @@ class Script: Variable(name): ast for name, ast in self._variables.items() } - unresolved_variables: Set[Variable] = set(variables.keys()) - resolved_variables: Dict[Variable, Resolvable] = ( - pre_resolved_variables if pre_resolved_variables else {} - ) + resolved_variables: Dict[Variable, Resolvable] = { + Variable(name): value for name, value in (pre_resolved_variables or {}).items() + } + unresolved_variables: Set[Variable] = set(variables.keys()) - set(resolved_variables.keys()) while unresolved_variables: unresolved_count: int = len(unresolved_variables) @@ -221,3 +221,45 @@ class Script: raise UNREACHABLE return {variable.name: resolvable for variable, resolvable in resolved_variables.items()} + + def partial_resolve(self, unresolvable: Set[str]) -> Dict[str, Resolvable]: + """ + Parameters + ---------- + unresolvable + Variables that cannot be resolved + + Returns + ------- + Dict of resolved values + """ + variables: Dict[Variable, SyntaxTree] = { + Variable(name): ast for name, ast in self._variables.items() + } + + unresolvable_variables: Set[Variable] = set(Variable(var) for var in unresolvable) + unresolved_variables: Set[Variable] = set(variables.keys()) - unresolvable_variables + resolved_variables: Dict[Variable, Resolvable] = {} + + while unresolved_variables: + unresolved_count: int = len(unresolved_variables) + + for variable in copy.deepcopy(unresolved_variables): + if variables[variable].variables.intersection(unresolvable_variables): + unresolvable_variables.add(variable) + unresolved_variables.remove(variable) + elif not variables[variable].has_variable_dependency( + resolved_variables=resolved_variables + ): + resolved_variables[variable] = variables[variable].resolve( + resolved_variables=resolved_variables, + custom_functions=self._functions, + ) + unresolved_variables.remove(variable) + + if len(unresolved_variables) == unresolved_count: + # Implies a cycle within the variables. Should never reach + # since cycles are detected in __init__ + raise UNREACHABLE + + return {variable.name: resolvable for variable, resolvable in resolved_variables.items()} diff --git a/tests/unit/script/test_script.py b/tests/unit/script/test_script.py index 13db0129..c2052b50 100644 --- a/tests/unit/script/test_script.py +++ b/tests/unit/script/test_script.py @@ -1,14 +1,9 @@ -import re - -import pytest - from ytdl_sub.script.script import Script from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.utils.exceptions import CycleDetected -class TestSyntaxTree: - def test_custom_function(self): +class TestScript: + def test_pre_resolved(self): assert Script( { "%custom_func": "return {[$0, $1]}", @@ -16,39 +11,18 @@ class TestSyntaxTree: "bb": "b", "cc": "{%custom_func(aa, bb)}", } - ).resolve() == {"aa": String("a"), "bb": String("b"), "cc": String('return ["a", "b"]')} - - def test_simple(self): - assert Script({"a": "a", "b": "{b_}", "b_": "b"}).resolve() == { - "a": String("a"), - "b": String("b"), - "b_": String("b"), + ).resolve(pre_resolved_variables={"bb": String("bb_override")}) == { + "aa": String("a"), + "bb": String("bb_override"), + "cc": String('return ["a", "bb_override"]'), } - def test_multiple_variables(self): - assert Script({"a": "a", "b": "b", "b_": " {a} {b} "}).resolve() == { - "a": String("a"), - "b": String("b"), - "b_": String(" a b "), - } - - def test_simple_with_function(self): - assert Script({"a": "a", "b": "{%capitalize(b_)}", "b_": "b"}).resolve() == { - "a": String("a"), - "b": String("B"), - "b_": String("b"), - } - - def test_simple_cycle(self): - with pytest.raises( - CycleDetected, - match=re.escape("Cycle detected within these variables: " "a -> b -> a"), - ): - Script({"a": "{b}", "b": "{a}"}).resolve() - - def test_simple_cycle_with_function(self): - with pytest.raises( - CycleDetected, - match=re.escape("Cycle detected within these variables: " "b -> b_ -> b"), - ): - Script({"b": "{%capitalize(b_)}", "b_": "{b}"}).resolve() + def test_partial_resolve(self): + assert Script( + { + "%custom_func": "return {[$0, $1]}", + "aa": "a", + "bb": "b", + "cc": "{%custom_func(aa, bb)}", + } + ).partial_resolve(unresolvable={"bb"}) == {"aa": String("a")}