diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index a6109f4d..3b6644f4 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -498,6 +498,8 @@ class _Parser: raise MAP_KEY_WITH_NO_VALUE if isinstance(key, NonHashable): raise MAP_KEY_NOT_HASHABLE + if isinstance(key, BuiltInFunction) and issubclass(key.output_type(), NonHashable): + raise MAP_KEY_NOT_HASHABLE if len(value_args) > 1: raise MAP_KEY_MULTIPLE_VALUES diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 7d1cea85..a3b39a9c 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -291,3 +291,6 @@ class BuiltInFunction(Function, BuiltInFunctionType): raise FunctionRuntimeException( f"Runtime error occurred when executing the function %{self.name}: {str(exc)}" ) from exc + + def __hash__(self): + return hash((self.name, *self.args)) diff --git a/src/ytdl_sub/utils/script.py b/src/ytdl_sub/utils/script.py index 9a9b48af..317adced 100644 --- a/src/ytdl_sub/utils/script.py +++ b/src/ytdl_sub/utils/script.py @@ -66,7 +66,9 @@ class ScriptUtils: ast = parse(text=value).ast if len(ast) == 1: return ast[0] - return BuiltInFunction(name="concat", args=ast) + return BuiltInFunction( + name="concat", args=[BuiltInFunction(name="string", args=[arg]) for arg in ast] + ) if isinstance(value, int): return Integer(value) if isinstance(value, float): diff --git a/tests/unit/script/types/test_map.py b/tests/unit/script/types/test_map.py index 3a6608cf..576eba35 100644 --- a/tests/unit/script/types/test_map.py +++ b/tests/unit/script/types/test_map.py @@ -198,3 +198,16 @@ class TestMap: "key_variable": "{['non-hashable']}", } ).resolve() + + def test_map_key_is_function(self): + assert Script( + { + "dict": "{{ %concat('hi', %string(' world')) : 'value' }}", + "key_variable": "hashable", + } + ).resolve() == ScriptOutput( + { + "key_variable": String("hashable"), + "dict": Map(value={String(value="hi world"): String(value="value")}), + } + ) diff --git a/tests/unit/validators/test_string_formatter_validator.py b/tests/unit/validators/test_string_formatter_validator.py index 7a0f9f1d..21ecc509 100644 --- a/tests/unit/validators/test_string_formatter_validator.py +++ b/tests/unit/validators/test_string_formatter_validator.py @@ -97,6 +97,7 @@ class TestUnstructuredDictFormatterValidator(object): key5_bool = True key6_map = {"{variable}_key": "value", "static_key": "{variable}_value"} key7_list = ["list_1", "list_{variable_2}"] + key8_many_vars = "string {variable1} with multiple {variable2}" validator = dict_validator_class( name="validator", value={ @@ -107,17 +108,19 @@ class TestUnstructuredDictFormatterValidator(object): "key5": key5_bool, "key6": key6_map, "key7": key7_list, + "key8": key8_many_vars, }, ) - assert len(validator.dict) == 7 + assert len(validator.dict) == 8 assert all(isinstance(val, expected_formatter_class) for val in validator.dict.values()) assert validator.dict_with_format_strings == { - "key1": "string with {variable}", + "key1": "{ %concat( %string( '''string with ''' ), %string( variable ) ) }", "key2": "no variables", - "key3": "{%int(3)}", - "key4": "{%float(4.132)}", - "key5": "{%int(True)}", - "key6": '{%from_json(\'\'\'{"static_key": "{variable}_value", "{variable}_key": "value"}\'\'\')}', - "key7": "{%from_json('''[\"list_1\", \"list_{variable_2}\"]''')}", + "key3": "{ %int(3) }", + "key4": "{ %float(4.132) }", + "key5": "{ %int(True) }", + "key6": "{ { %concat( %string( variable ), %string( '''_key''' ) ): '''value''', '''static_key''': %concat( %string( variable ), %string( '''_value''' ) ) } }", + "key7": "{ [ '''list_1''', %concat( %string( '''list_''' ), %string( variable_2 ) ) ] }", + "key8": "{ %concat( %string( '''string ''' ), %string( variable1 ), %string( ''' with multiple ''' ), %string( variable2 ) ) }", }