From a01c658fed87a58b5510e3c80547d3d2a0e7dc37 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Thu, 9 Nov 2023 18:47:56 -0800 Subject: [PATCH] tesssstssss --- src/ytdl_sub/script/parser.py | 22 +++++-- src/ytdl_sub/script/script.py | 2 +- src/ytdl_sub/script/types/array.py | 3 + src/ytdl_sub/script/types/map.py | 17 ++++++ .../script/{ => types}/syntax_tree.py | 0 tests/unit/script/test_parser.py | 60 ++++++++++--------- tests/unit/script/test_script.py | 18 +++--- tests/unit/script/test_syntax_tree.py | 57 ------------------ tests/unit/script/types/__init__.py | 0 tests/unit/script/types/test_array.py | 48 +++++++++++++++ tests/unit/script/types/test_map.py | 56 +++++++++++++++++ 11 files changed, 186 insertions(+), 97 deletions(-) rename src/ytdl_sub/script/{ => types}/syntax_tree.py (100%) delete mode 100644 tests/unit/script/test_syntax_tree.py create mode 100644 tests/unit/script/types/__init__.py create mode 100644 tests/unit/script/types/test_array.py create mode 100644 tests/unit/script/types/test_map.py diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 566004a4..bd545735 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -2,8 +2,6 @@ from typing import Dict 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 @@ -12,6 +10,7 @@ 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 String +from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import Variable from ytdl_sub.utils.exceptions import StringFormattingException @@ -225,6 +224,7 @@ class _Parser: """ output: Dict[ArgumentType, ArgumentType] = {} key: Optional[ArgumentType] = None + in_comma = False while ch := self._read(increment_pos=False): if ch == "}": @@ -233,7 +233,17 @@ class _Parser: self._pos += 1 return UnresolvedMap(value=output) + elif ch == ",": + if in_comma: + raise StringFormattingException("Comma followed by comma") + if key is not None: + raise StringFormattingException("key followed by comma") + if output is None: + raise StringFormattingException("Empty dict with comma") + in_comma = True + self._pos += 1 elif key is None: + in_comma = False key_args = self._parse_args(breaking_chars=":") if len(key_args) != 1: raise StringFormattingException("Lazy parsing but got mlutiple args") @@ -255,7 +265,7 @@ class _Parser: while ch := self._read(): if ch == "}": bracket_counter -= 1 - break + continue if ch == "{": bracket_counter += 1 if literal_str: @@ -284,8 +294,12 @@ class _Parser: self._ast.append(self._parse_map()) else: self._ast.append(self._parse_variable()) - else: + elif bracket_counter == 0: + # Only accumulate literal str if not in brackets literal_str += ch + else: + # Should only be possible to get here if it's a space + assert ch.isspace() if bracket_counter != 0: raise StringFormattingException("Bracket count mismatch") diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index f73dd870..8ae449cb 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -2,8 +2,8 @@ from typing import Dict from typing import Optional from ytdl_sub.script.parser import parse -from ytdl_sub.script.syntax_tree import SyntaxTree 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 diff --git a/src/ytdl_sub/script/types/array.py b/src/ytdl_sub/script/types/array.py index b11e6672..47dfb48d 100644 --- a/src/ytdl_sub/script/types/array.py +++ b/src/ytdl_sub/script/types/array.py @@ -14,6 +14,9 @@ from ytdl_sub.script.types.variable_dependency import VariableDependency class Array: value: List[Resolvable] + def __str__(self): + return f"[{', '.join([str(val.value) for val in self.value])}]" + @dataclass(frozen=True) class UnresolvedArray(Array, VariableDependency, ArgumentType): diff --git a/src/ytdl_sub/script/types/map.py b/src/ytdl_sub/script/types/map.py index a76976da..30f6ec26 100644 --- a/src/ytdl_sub/script/types/map.py +++ b/src/ytdl_sub/script/types/map.py @@ -1,3 +1,4 @@ +import json from dataclasses import dataclass from typing import Dict from typing import List @@ -16,6 +17,22 @@ from ytdl_sub.utils.exceptions import StringFormattingException class Map: value: Dict[Hashable, Resolvable] + def to_native(self) -> Dict: + output = {} + for key, value in self.value.items(): + native_key = key.value + if isinstance(value, Map): + native_value = value.to_native() + else: + native_value = value.value + + output[native_key] = native_value + + return output + + def __str__(self): + return json.dumps(self.to_native()) + @dataclass(frozen=True) class UnresolvedMap(Map, VariableDependency, ArgumentType): diff --git a/src/ytdl_sub/script/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py similarity index 100% rename from src/ytdl_sub/script/syntax_tree.py rename to src/ytdl_sub/script/types/syntax_tree.py diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index 9b870b6a..26435f4d 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -1,14 +1,15 @@ +from typing import Optional from typing import Union import pytest from ytdl_sub.script.parser import parse -from ytdl_sub.script.syntax_tree import SyntaxTree from ytdl_sub.script.types.function import BuiltInFunction 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 String +from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import Variable from ytdl_sub.utils.exceptions import StringFormattingException @@ -28,28 +29,28 @@ class TestParser: ] ) - def test_array(self): - parsed = parse("hello {['elem1', 'elem2']}") - parsed_empty = parse("hello {[]}") - parsed_with_var = parse("hello {['elem1', variable_name]}") - 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): - parsed = parse("hello {%map(['elem1', 'elem2'])}") - parsed_empty = parse("hello {%map()}") - parsed_with_var = parse("hello {%map([variable_name, 'elem2'])}") - parsed_extend = parse("hi {%map([variable_name, 'elem2'], ['elem3', variable_name])}") - parse_raw_map = parse("hello {{'key': 'value'}}") - parsed_extend.resolve({}) - assert False - - def test_function_argument(self): - parsed = parse("hello {%map([$1, $2])}") - assert False + # def test_array(self): + # parsed = parse("hello {['elem1', 'elem2']}") + # parsed_empty = parse("hello {[]}") + # parsed_with_var = parse("hello {['elem1', variable_name]}") + # 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): + # parsed = parse("hello {%map(['elem1', 'elem2'])}") + # parsed_empty = parse("hello {%map()}") + # parsed_with_var = parse("hello {%map([variable_name, 'elem2'])}") + # parsed_extend = parse("hi {%map([variable_name, 'elem2'], ['elem3', variable_name])}") + # parse_raw_map = parse("hello {{'key': 'value'}}") + # parsed_extend.resolve({}) + # assert False + # + # def test_function_argument(self): + # parsed = parse("hello {%map([$1, $2])}") + # assert False def test_conditional(self): parsed = parse("hello {%if(True, 'hi', 3.4)}") @@ -141,13 +142,18 @@ class TestParser: ] ) - @pytest.mark.parametrize("whitespace", ["", " ", " ", "\n", " \n "]) - def test_single_function_multiple_args(self, whitespace: str): + @pytest.mark.parametrize("whitespace", [None, " ", " ", "\n", " \n "]) + def test_single_function_multiple_args(self, whitespace: Optional[str]): s = whitespace - parsed = parse( + if s is None: + s = "" + + input_str = ( f"hello{s}{{{s}%concat({s}'string'{s},{s}%string(1){s},{s}%string(2.4){s}," - f"{s}%string(TRUE){s},{s}%string(variable_name){s},{s}%capitalize({s}'hi'{s}){s}){s}}}" + f"{s}%string(TRUE){s},{s}%string(variable_name){s},{s}%capitalize({s}'hi'{s}){s})}}" + f"{s}" ) + parsed = parse(input_str) assert parsed == SyntaxTree( [ String(value=f"hello{s}"), diff --git a/tests/unit/script/test_script.py b/tests/unit/script/test_script.py index 37a15a23..5821da26 100644 --- a/tests/unit/script/test_script.py +++ b/tests/unit/script/test_script.py @@ -1,12 +1,7 @@ -from typing import Dict - import pytest from ytdl_sub.script.script import Script -from ytdl_sub.script.syntax_tree import SyntaxTree -from ytdl_sub.script.types.function import Function from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.types.variable import Variable from ytdl_sub.utils.exceptions import StringFormattingException @@ -19,17 +14,24 @@ class TestSyntaxTree: "bb": "b", "cc": "{%custom_func(aa, bb)}", } - ).resolve() == {"aa": String("a"), "bb": String("b"), "cc": String("return [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() == { + assert Script({"a": "a", "b": "{b_}", "b_": "b"}).resolve() == { "a": String("a"), "b": String("b"), "b_": String("b"), } + 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() == { + assert Script({"a": "a", "b": "{%capitalize(b_)}", "b_": "b"}).resolve() == { "a": String("a"), "b": String("B"), "b_": String("b"), diff --git a/tests/unit/script/test_syntax_tree.py b/tests/unit/script/test_syntax_tree.py deleted file mode 100644 index 5abc6998..00000000 --- a/tests/unit/script/test_syntax_tree.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Dict - -import pytest - -from ytdl_sub.script.syntax_tree import SyntaxTree -from ytdl_sub.script.types.function import Function -from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.types.variable import Variable -from ytdl_sub.utils.exceptions import StringFormattingException - - -class TestSyntaxTree: - def test_simple(self): - overrides: Dict[str, SyntaxTree] = { - "a": SyntaxTree(ast=[String("a")]), - "b": SyntaxTree(ast=[Variable("b_")]), - "b_": SyntaxTree(ast=[String("b")]), - } - - resolved = SyntaxTree.resolve_overrides(parsed_overrides=overrides) - assert resolved == { - "a": String(value="a"), - "b": String(value="b"), - "b_": String(value="b"), - } - - def test_simple_with_function(self): - overrides: Dict[str, SyntaxTree] = { - "a": SyntaxTree(ast=[String("a")]), - "b": SyntaxTree(ast=[Function(name="capitalize", args=[Variable("b_")])]), - "b_": SyntaxTree(ast=[String("b")]), - } - - resolved = SyntaxTree.resolve_overrides(parsed_overrides=overrides) - assert resolved == { - "a": String(value="a"), - "b": String(value="B"), - "b_": String(value="b"), - } - - def test_simple_cycle(self): - overrides: Dict[str, SyntaxTree] = { - "a": SyntaxTree(ast=[Variable("b")]), - "b": SyntaxTree(ast=[Variable("a")]), - } - - with pytest.raises(StringFormattingException): - _ = SyntaxTree.resolve_overrides(parsed_overrides=overrides) - - def test_simple_cycle_with_function(self): - overrides: Dict[str, SyntaxTree] = { - "a": SyntaxTree(ast=[String("a")]), - "b": SyntaxTree(ast=[Function(name="capitalize", args=[Variable("b_")])]), - "b_": SyntaxTree(ast=[Variable("b")]), - } - with pytest.raises(StringFormattingException): - _ = SyntaxTree.resolve_overrides(parsed_overrides=overrides) diff --git a/tests/unit/script/types/__init__.py b/tests/unit/script/types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/script/types/test_array.py b/tests/unit/script/types/test_array.py new file mode 100644 index 00000000..12c5d1a8 --- /dev/null +++ b/tests/unit/script/types/test_array.py @@ -0,0 +1,48 @@ +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.array import ResolvedArray +from ytdl_sub.script.types.resolvable import Float +from ytdl_sub.script.types.resolvable import String + + +class TestArray: + def test_return(self): + assert Script({"array": "{['a', 3.14]}"}).resolve() == { + "array": ResolvedArray([String("a"), Float(3.14)]) + } + + def test_return_as_str(self): + assert Script({"array": "str: {['a', 3.14]}"}).resolve() == { + "array": String("str: [a, 3.14]") + } + + def test_nested_array(self): + assert Script( + {"array": "{['level1', ['level2', ['level3', 'level3'], 'level2'], 'level1']}"} + ).resolve() == { + "array": ResolvedArray( + [ + String("level1"), + ResolvedArray( + [ + String("level2"), + ResolvedArray([String("level3"), String("level3")]), + String("level2"), + ], + ), + String("level1"), + ] + ) + } + + def test_empty_array(self): + assert Script({"array": "{[]}"}).resolve() == {"array": ResolvedArray([])} + + def test_custom_function(self): + assert Script( + { + "%custom_func": "return {[$1, $2]}", + "aa": "a", + "bb": "b", + "cc": "{%custom_func(aa, bb)}", + } + ).resolve() == {"aa": String("a"), "bb": String("b"), "cc": String("return [a, b]")} diff --git a/tests/unit/script/types/test_map.py b/tests/unit/script/types/test_map.py new file mode 100644 index 00000000..4e316a91 --- /dev/null +++ b/tests/unit/script/types/test_map.py @@ -0,0 +1,56 @@ +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.map import ResolvedMap +from ytdl_sub.script.types.resolvable import Float +from ytdl_sub.script.types.resolvable import String + + +class TestMap: + def test_return(self): + assert Script({"map": "{{'a': 3.14}}"}).resolve() == { + "map": ResolvedMap({String("a"): Float(3.14)}) + } + + def test_return_as_str(self): + assert Script({"map": "json: {{'a': 3.14}}"}).resolve() == { + "map": String('json: {"a": 3.14}') + } + + def test_nested_map(self): + map_str = """{ + { + 'level1': { + 'level2': { + 'level3': { + 'level4_key': 'level4_value' + }, + 'level3_key': 'level3_value' + }, + 'level2_key': 'level2_value' + }, + 'level1_key': 'level1_value' + } + }""" + + assert Script({"map": map_str}).resolve() == { + "map": ResolvedMap( + { + String("level1"): ResolvedMap( + { + String("level2"): ResolvedMap( + { + String("level3"): ResolvedMap( + {String("level4_key"): String("level4_value")} + ), + String("level3_key"): String("level3_value"), + } + ), + String("level2_key"): String("level2_value"), + } + ), + String("level1_key"): String("level1_value"), + } + ) + } + + def test_empty_map(self): + assert Script({"map": "{{}}"}).resolve() == {"map": ResolvedMap({})}