slick script class

This commit is contained in:
Jesse Bannon 2023-11-23 00:31:59 -08:00
parent 0477d29f44
commit 0735d8a8ac
4 changed files with 74 additions and 21 deletions

View file

@ -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 UNREACHABLE
from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import CycleDetected
from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments 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 from ytdl_sub.script.utils.name_validation import validate_variable_name
# pylint: disable=missing-raises-doc # pylint: disable=missing-raises-doc
@ -143,6 +144,12 @@ class Script:
f"{nested_custom_function.num_input_args}" 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]): def __init__(self, script: Dict[str, str]):
function_names: Set[str] = { function_names: Set[str] = {
self._function_name(name) for name in script.keys() if self._is_function(name) 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() for variable_key, variable_value in script.items()
if not self._is_function(variable_key) if not self._is_function(variable_key)
} }
self._validate()
self._ensure_no_custom_function_cycles() def _update_internally(self, resolved_variables: Dict[str, Resolvable]) -> None:
self._ensure_custom_function_arguments_valid() for variable_name, resolved in resolved_variables.items():
self._ensure_no_variable_cycles() self._variables[variable_name] = SyntaxTree(ast=[resolved])
self._ensure_custom_function_usage_num_input_arguments_valid()
def resolve( def resolve(
self, self,
resolved: Optional[Dict[str, Resolvable]] = None, resolved: Optional[Dict[str, Resolvable]] = None,
unresolvable: Optional[Set[str]] = None, unresolvable: Optional[Set[str]] = None,
update: bool = False,
) -> Dict[str, Resolvable]: ) -> Dict[str, Resolvable]:
""" """
Parameters Parameters
---------- ----------
resolved 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 unresolvable
Variables that cannot be resolved, forcing any variable that depends on it to not be Optional. Variables that cannot be resolved, forcing any variable that depends on it
resolved. to not be resolved.
update
Optional. Whether to update the internal representation of variables with their
resolved value (if they get resolved).
Returns Returns
------- -------
@ -232,4 +243,32 @@ class Script:
# since cycles are detected in __init__ # since cycles are detected in __init__
raise UNREACHABLE 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}")

View file

@ -1,6 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional
from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import Argument
from ytdl_sub.script.types.resolvable import Resolvable 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 # Otherwise, to concat multiple resolved outputs, we must concat as strings
return String("".join([str(res) for res in resolved])) 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

View file

@ -7,6 +7,10 @@ class UserException(ValidationException, ABC):
"""It's the user's fault!""" """It's the user's fault!"""
class RuntimeException(ValueError, ABC):
"""Exception thrown at runtime during resolution"""
class InvalidSyntaxException(UserException): class InvalidSyntaxException(UserException):
"""Syntax is incorrect""" """Syntax is incorrect"""
@ -43,10 +47,6 @@ class CycleDetected(UserException):
"""A cycle exists within a user's script""" """A cycle exists within a user's script"""
class RuntimeException(ValueError, ABC):
"""Exception thrown at runtime during resolution"""
class FunctionRuntimeException(RuntimeException): class FunctionRuntimeException(RuntimeException):
"""Exception thrown when a ytdl-sub function has an error occur at runtime""" """Exception thrown when a ytdl-sub function has an error occur at runtime"""

View file

@ -34,16 +34,23 @@ class TestScript:
"entry": "{%throw('entry has not been populated yet')}", "entry": "{%throw('entry has not been populated yet')}",
"title": "{%map_get(entry, 'title')}", "title": "{%map_get(entry, 'title')}",
"override": "hi", "override": "hi",
"resolved_override": "{override} mom",
} }
) )
overrides = script.resolve(unresolvable={"entry"}) script.resolve(unresolvable={"entry"}, update=True)
assert overrides == {"override": String("hi")} assert script.get("override") == String("hi")
assert script.get("resolved_override") == String("hi mom")
entry_map = ResolvedMap({String("title"): String("the title")}) script.add(
entry_output = script.resolve(resolved=dict(overrides, **{"entry": entry_map})) {
assert entry_output == { "new_variable": "{resolved_override} {title}",
"override": String("hi"), "new_variable_upper": "{%upper(new_variable)}",
"entry": entry_map,
"title": String("the title"),
} }
).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")