diff --git a/src/ytdl_sub/script/functions/array_functions.py b/src/ytdl_sub/script/functions/array_functions.py index b75a6ade..9a2bf2fb 100644 --- a/src/ytdl_sub/script/functions/array_functions.py +++ b/src/ytdl_sub/script/functions/array_functions.py @@ -1,6 +1,7 @@ from typing import List -from ytdl_sub.script.types.resolvable import Array +from ytdl_sub.script.types.array import Array +from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Resolvable @@ -12,3 +13,18 @@ class ArrayFunctions: output.extend(array.value) return Array(output) + + @staticmethod + def at(array: Array, idx: Integer) -> Resolvable: + return array.value[idx.value] + + @staticmethod + def flatten_array(array: Array) -> Array: + output: List[Resolvable] = [] + for elem in array.value: + if isinstance(elem, Array): + output.extend(ArrayFunctions.flatten_array(elem).value) + else: + output.append(elem) + + return Array(output) diff --git a/src/ytdl_sub/script/functions/map_functions.py b/src/ytdl_sub/script/functions/map_functions.py index d5ef7010..51f4828c 100644 --- a/src/ytdl_sub/script/functions/map_functions.py +++ b/src/ytdl_sub/script/functions/map_functions.py @@ -1,6 +1,8 @@ -from typing import List, Dict +from typing import Dict +from typing import List -from ytdl_sub.script.types.resolvable import Array, Map +from ytdl_sub.script.types.array import Array +from ytdl_sub.script.types.resolvable import Map from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.utils.exceptions import StringFormattingException diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index f11185a4..8566ae58 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -2,9 +2,10 @@ from typing import List from typing import Optional from ytdl_sub.script.syntax_tree import SyntaxTree +from ytdl_sub.script.types.array import Array +from ytdl_sub.script.types.array import UnresolvedArray from ytdl_sub.script.types.function import ArgumentType from ytdl_sub.script.types.function import Function -from ytdl_sub.script.types.resolvable import Array from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer @@ -20,7 +21,7 @@ class _Parser: def __init__(self, text: str): self._text = text self._pos = 0 - self._ast: List[String | Variable | Function] = [] + self._ast: List[ArgumentType] = [] self._syntax_tree = self._parse() @@ -157,7 +158,7 @@ class _Parser: Begin parsing a function after reading the first ``%`` """ function_name: str = "" - function_args: List[String | Variable | "Function"] = [] + function_args: List[ArgumentType] = [] while ch := self._read(): if ch == ")": @@ -170,16 +171,16 @@ class _Parser: raise StringFormattingException("Invalid function") - def _parse_array(self) -> Array: + def _parse_array(self) -> UnresolvedArray: """ Begin parsing an array after reading the first ``[`` """ - function_args: List[String | Variable | "Function"] = [] + function_args: List[ArgumentType] = [] while ch := self._read(increment_pos=False): if ch == "]": self._pos += 1 - return Array(value=function_args) + return UnresolvedArray(value=function_args) else: function_args = self._parse_args(breaking_char="]") diff --git a/src/ytdl_sub/script/syntax_tree.py b/src/ytdl_sub/script/syntax_tree.py index a24621c7..317d701a 100644 --- a/src/ytdl_sub/script/syntax_tree.py +++ b/src/ytdl_sub/script/syntax_tree.py @@ -1,20 +1,20 @@ from dataclasses import dataclass from typing import Dict from typing import List -from typing import Optional from typing import Set from ytdl_sub.script.types.function import Function -from ytdl_sub.script.types.function import VariableDependency +from ytdl_sub.script.types.resolvable import ArgumentType 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.utils.exceptions import StringFormattingException @dataclass(frozen=True) class SyntaxTree(VariableDependency): - ast: List[String | Variable | Function] + ast: List[ArgumentType] @property def variables(self) -> Set[Variable]: @@ -35,14 +35,9 @@ class SyntaxTree(VariableDependency): def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable: resolved: List[Resolvable] = [] for token in self.ast: - if isinstance(token, Resolvable): - resolved.append(token) - elif isinstance(token, Variable): - resolved.append(resolved_variables[token]) - elif isinstance(token, Function): - resolved.append(token.resolve(resolved_variables=resolved_variables)) - else: - assert False, "should never reach" + resolved.append( + self._resolve_argument_type(resolved_variables=resolved_variables, arg=token) + ) # If only one resolvable resides in the AST, return as that if len(resolved) == 1: diff --git a/src/ytdl_sub/script/types/array.py b/src/ytdl_sub/script/types/array.py new file mode 100644 index 00000000..a2aaa77c --- /dev/null +++ b/src/ytdl_sub/script/types/array.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from typing import Dict +from typing import List +from typing import Set + +from ytdl_sub.script.types.resolvable import ArgumentType +from ytdl_sub.script.types.resolvable import Resolvable +from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.variable_dependency import VariableDependency + + +@dataclass(frozen=True) +class Array: + value: List[Resolvable] + + +@dataclass(frozen=True) +class UnresolvedArray(Array, VariableDependency, ArgumentType): + value: List[ArgumentType] + + @property + def variables(self) -> Set[Variable]: + return {value for value in self.value if isinstance(value, Variable)} + + def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable: + return ResolvedArray( + [ + self._resolve_argument_type(resolved_variables=resolved_variables, arg=arg) + for arg in self.value + ] + ) + + +@dataclass(frozen=True) +class ResolvedArray(Array, Resolvable): + pass diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index b3367819..c524e01c 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -1,7 +1,5 @@ import functools import inspect -from abc import ABC -from abc import abstractmethod from dataclasses import dataclass from inspect import FullArgSpec from typing import Callable @@ -11,45 +9,18 @@ from typing import Optional from typing import Set from typing import Type from typing import Union -from typing import final from typing import get_origin from ytdl_sub.script.functions import Functions -from ytdl_sub.script.types.resolvable import Array -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.types.resolvable import ArgumentType from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable_0 from ytdl_sub.script.types.resolvable import Resolvable_1 from ytdl_sub.script.types.resolvable import Resolvable_2 -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.utils.exceptions import StringFormattingException -ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function", Array] - - -@dataclass(frozen=True) -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 not self.variables.issubset(set(resolved_variables.keys())) - def is_union(arg_type: Type) -> bool: return get_origin(arg_type) is Union @@ -151,7 +122,7 @@ class FunctionInputSpec: @dataclass(frozen=True) -class Function(VariableDependency): +class Function(VariableDependency, ArgumentType): name: str args: List[ArgumentType] @@ -227,13 +198,9 @@ class Function(VariableDependency): return variables def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable: - resolved_args: List[Resolvable] = [] - for arg in self.args: - if arg in resolved_variables: - resolved_args.append(resolved_variables[arg]) - elif isinstance(arg, Function): - resolved_args.append(arg.resolve(resolved_variables)) - else: - resolved_args.append(arg) + resolved_args = [ + self._resolve_argument_type(resolved_variables=resolved_variables, arg=arg) + for arg in self.args + ] return self.callable(*resolved_args) diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index 62630215..9f4016db 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -1,15 +1,18 @@ from abc import ABC -from abc import abstractmethod from dataclasses import dataclass -from typing import Any, Dict +from typing import Any +from typing import Dict from typing import Generic -from typing import List from typing import TypeVar T = TypeVar("T") NumericT = TypeVar("NumericT", bound=int | float) +class ArgumentType(ABC): + pass + + class Resolvable_0(ABC): pass @@ -41,35 +44,28 @@ class Numeric(ResolvableT[NumericT], ABC, Generic[NumericT]): @dataclass(frozen=True) -class Integer(Numeric[int]): +class Integer(Numeric[int], ArgumentType): pass @dataclass(frozen=True) -class Float(Numeric[float]): +class Float(Numeric[float], ArgumentType): pass @dataclass(frozen=True) -class Boolean(ResolvableT[bool]): +class Boolean(ResolvableT[bool], ArgumentType): pass @dataclass(frozen=True) -class String(ResolvableT[str]): +class String(ResolvableT[str], ArgumentType): pass @dataclass(frozen=True) -class Array(Resolvable): - value: List[Resolvable] - - def __str__(self) -> str: - return f"[{', '.join([val.value for val in self.value])}]" - -@dataclass(frozen=True) -class Map(Resolvable): +class Map(Resolvable, ArgumentType): value: Dict[Resolvable, Resolvable] def __str__(self) -> str: - return f"[{', '.join([val.value for val in self.value])}]" \ No newline at end of file + return f"[{', '.join([val.value for val in self.value])}]" diff --git a/src/ytdl_sub/script/types/variable.py b/src/ytdl_sub/script/types/variable.py index 4bcb8b63..af723724 100644 --- a/src/ytdl_sub/script/types/variable.py +++ b/src/ytdl_sub/script/types/variable.py @@ -1,6 +1,8 @@ from dataclasses import dataclass +from ytdl_sub.script.types.resolvable import ArgumentType + @dataclass(frozen=True) -class Variable: +class Variable(ArgumentType): name: str diff --git a/src/ytdl_sub/script/types/variable_dependency.py b/src/ytdl_sub/script/types/variable_dependency.py new file mode 100644 index 00000000..b08f821b --- /dev/null +++ b/src/ytdl_sub/script/types/variable_dependency.py @@ -0,0 +1,46 @@ +from abc import ABC +from abc import abstractmethod +from dataclasses import dataclass +from typing import Dict +from typing import Set +from typing import final + +from ytdl_sub.script.types.resolvable import ArgumentType +from ytdl_sub.script.types.resolvable import Resolvable +from ytdl_sub.script.types.variable import Variable +from ytdl_sub.utils.exceptions import StringFormattingException + + +@dataclass(frozen=True) +class VariableDependency(ABC): + @property + @abstractmethod + def variables(self) -> Set[Variable]: + raise NotImplemented() + + @abstractmethod + def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable: + raise NotImplemented() + + def _resolve_argument_type( + self, resolved_variables: Dict[Variable, Resolvable], arg: ArgumentType + ) -> Resolvable: + if isinstance(arg, Resolvable): + return arg + if isinstance(arg, Variable): + if arg not in resolved_variables: + raise StringFormattingException("should never reach@") + return resolved_variables[arg] + if isinstance(arg, VariableDependency): + return arg.resolve(resolved_variables) + + assert False, "never reach here" + + @final + def has_variable_dependency(self, resolved_variables: Dict[Variable, Resolvable]) -> bool: + """ + Returns + ------- + True if variable dependency. False otherwise. + """ + return not self.variables.issubset(set(resolved_variables.keys())) diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index 7a50113a..f638bd30 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -32,7 +32,10 @@ class TestParser: parsed = parse("hello {['elem1', 'elem2']}") parsed_empty = parse("hello {[]}") parsed_with_var = parse("hello {['elem1', variable_name]}") - parsed_extend = parse("hi {%extend(['elem1', 'elem2'], ['elem3'], [], ['elem4'])}") + parsed_extend = parse( + "hi {%at(%flatten_array(%extend(['elem1', 'elem2'], ['elem3'], [['elem4'], ['elem5', 'elem6']], ['elem7'])), 1)}" + ) + parsed_extend.resolve({}) assert False def test_map(self):