OverridesResolver
This commit is contained in:
parent
fe095b151c
commit
ebf7989681
4 changed files with 147 additions and 11 deletions
65
src/ytdl_sub/script/overrides_resolver.py
Normal file
65
src/ytdl_sub/script/overrides_resolver.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.script.functions import Resolvable
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.script.syntax_tree import SyntaxTree
|
||||
from ytdl_sub.script.syntax_tree import Variable
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
|
||||
|
||||
class OverridesResolver:
|
||||
def __init__(self, overrides: Dict[str, str]):
|
||||
self.overrides: Dict[Variable, SyntaxTree] = {
|
||||
Variable(name=name): parse(value) for name, value in overrides.items()
|
||||
}
|
||||
|
||||
def _ensure_no_cycles(self) -> None:
|
||||
variable_dependencies: Dict[Variable, Set[Variable]] = {
|
||||
variable: ast.variables for variable, ast in self.overrides.items()
|
||||
}
|
||||
|
||||
def _traverse(
|
||||
to_variable: Variable, visited_variables: Optional[List[Variable]] = None
|
||||
) -> None:
|
||||
if visited_variables is None:
|
||||
visited_variables = []
|
||||
|
||||
if to_variable in visited_variables:
|
||||
raise StringFormattingException("Detected cycle in variables")
|
||||
visited_variables.append(to_variable)
|
||||
|
||||
for dep in variable_dependencies[to_variable]:
|
||||
_traverse(to_variable=dep, visited_variables=visited_variables)
|
||||
|
||||
for variable in variable_dependencies.keys():
|
||||
_traverse(variable)
|
||||
|
||||
def resolve_overrides(self) -> Dict[str, str]:
|
||||
self._ensure_no_cycles()
|
||||
|
||||
unresolved_variables: List[Variable] = list(self.overrides.keys())
|
||||
resolved_variables: Dict[Variable, Resolvable] = {}
|
||||
|
||||
while unresolved_variables:
|
||||
unresolved_count: int = len(unresolved_variables)
|
||||
|
||||
for variable in unresolved_variables:
|
||||
if not self.overrides[variable].has_variable_dependency(
|
||||
resolved_variables=resolved_variables
|
||||
):
|
||||
resolved_variables[variable] = self.overrides[variable].resolve(
|
||||
resolved_variables=resolved_variables
|
||||
)
|
||||
unresolved_variables.remove(variable)
|
||||
|
||||
assert (
|
||||
len(unresolved_variables) != unresolved_count
|
||||
), "did not resolve any variables, cycle detected"
|
||||
|
||||
return {
|
||||
variable.name: resolvable.resolve()
|
||||
for variable, resolvable in resolved_variables.items()
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@ from ytdl_sub.script.functions import Boolean
|
|||
from ytdl_sub.script.functions import Float
|
||||
from ytdl_sub.script.functions import Integer
|
||||
from ytdl_sub.script.functions import String
|
||||
from ytdl_sub.script.types import ArgumentType
|
||||
from ytdl_sub.script.types import Function
|
||||
from ytdl_sub.script.types import SyntaxTree
|
||||
from ytdl_sub.script.types import Variable
|
||||
from ytdl_sub.script.syntax_tree import ArgumentType
|
||||
from ytdl_sub.script.syntax_tree import Function
|
||||
from ytdl_sub.script.syntax_tree import SyntaxTree
|
||||
from ytdl_sub.script.syntax_tree import Variable
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
from ytdl_sub.validators.string_formatter_validators import is_valid_source_variable_name
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Union
|
||||
from typing import final
|
||||
|
||||
from ytdl_sub.script.functions import Boolean
|
||||
from ytdl_sub.script.functions import Float
|
||||
from ytdl_sub.script.functions import Functions
|
||||
from ytdl_sub.script.functions import Integer
|
||||
from ytdl_sub.script.functions import Resolvable
|
||||
from ytdl_sub.script.functions import String
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
|
||||
|
|
@ -22,11 +26,33 @@ ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"]
|
|||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Function:
|
||||
class VariableDependency(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def variables(self) -> Set[Variable]:
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> str:
|
||||
raise NotImplemented()
|
||||
|
||||
@final
|
||||
def has_variable_dependency(self, resolved_variables: Dict[Variable, Resolvable]) -> bool:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
True if variable dependency. False otherwise.
|
||||
"""
|
||||
return self.variables.issubset(set(resolved_variables.keys()))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Function(VariableDependency):
|
||||
name: str
|
||||
args: List[ArgumentType]
|
||||
|
||||
def __post_init__(self):
|
||||
# TODO: Figure out resolution via introspecting args and outputs of function
|
||||
try:
|
||||
getattr(Functions, self.name)
|
||||
except AttributeError:
|
||||
|
|
@ -48,9 +74,12 @@ class Function:
|
|||
|
||||
return variables
|
||||
|
||||
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable:
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SyntaxTree:
|
||||
class SyntaxTree(VariableDependency):
|
||||
ast: List[String | Variable | Function]
|
||||
|
||||
@property
|
||||
|
|
@ -69,6 +98,20 @@ class SyntaxTree:
|
|||
|
||||
return variables
|
||||
|
||||
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable:
|
||||
output: str = ""
|
||||
for token in self.ast:
|
||||
if isinstance(token, String):
|
||||
output += token.resolve()
|
||||
elif isinstance(token, Variable):
|
||||
output += resolved_variables[token].resolve()
|
||||
elif isinstance(token, Function):
|
||||
output += token.resolve(resolved_variables=resolved_variables)
|
||||
else:
|
||||
assert False, "should never reach"
|
||||
|
||||
return String(output)
|
||||
|
||||
@classmethod
|
||||
def detect_cycles(cls, parsed_overrides: Dict[str, "SyntaxTree"]) -> None:
|
||||
"""
|
||||
|
|
@ -98,5 +141,33 @@ class SyntaxTree:
|
|||
_traverse(variable)
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, parsed_overrides: Dict[str, "SyntaxTree"]) -> Dict[str, str]:
|
||||
raise NotImplemented()
|
||||
def resolve_overrides(cls, parsed_overrides: Dict[str, "SyntaxTree"]) -> Dict[str, str]:
|
||||
cls.detect_cycles(parsed_overrides=parsed_overrides)
|
||||
|
||||
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] = {}
|
||||
|
||||
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
|
||||
)
|
||||
unresolved_variables.remove(variable)
|
||||
|
||||
assert (
|
||||
len(unresolved_variables) != unresolved_count
|
||||
), "did not resolve any variables, cycle detected"
|
||||
|
||||
return {
|
||||
variable.name: resolvable.resolve()
|
||||
for variable, resolvable in resolved_variables.items()
|
||||
}
|
||||
|
|
@ -5,9 +5,9 @@ from ytdl_sub.script.functions import Float
|
|||
from ytdl_sub.script.functions import Integer
|
||||
from ytdl_sub.script.functions import String
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.script.types import Function
|
||||
from ytdl_sub.script.types import SyntaxTree
|
||||
from ytdl_sub.script.types import Variable
|
||||
from ytdl_sub.script.syntax_tree import Function
|
||||
from ytdl_sub.script.syntax_tree import SyntaxTree
|
||||
from ytdl_sub.script.syntax_tree import Variable
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue