diff --git a/omegaconf/grammar/OmegaConfGrammarLexer.g4 b/omegaconf/grammar/OmegaConfGrammarLexer.g4 index 251addfc2..4960ed02e 100644 --- a/omegaconf/grammar/OmegaConfGrammarLexer.g4 +++ b/omegaconf/grammar/OmegaConfGrammarLexer.g4 @@ -76,5 +76,5 @@ INTER_CLOSE: '}' -> popMode; DOT: '.'; INTER_ID: ID -> type(ID); -LIST_INDEX: INT_UNSIGNED; +INTER_KEY: ~[\\${}()[\]:. \t'"]+; // interpolation key, may contain any non special character INTER_WS: WS -> skip; diff --git a/omegaconf/grammar/OmegaConfGrammarParser.g4 b/omegaconf/grammar/OmegaConfGrammarParser.g4 index 50dda53a7..a90f3836b 100644 --- a/omegaconf/grammar/OmegaConfGrammarParser.g4 +++ b/omegaconf/grammar/OmegaConfGrammarParser.g4 @@ -49,7 +49,7 @@ sequence: element (COMMA element)*; interpolation: interpolationNode | interpolationResolver; interpolationNode: INTER_OPEN DOT* configKey (DOT configKey)* INTER_CLOSE; interpolationResolver: INTER_OPEN (interpolation | ID) COLON sequence? BRACE_CLOSE; -configKey: interpolation | ID | LIST_INDEX; +configKey: interpolation | ID | INTER_KEY; // Primitive types. diff --git a/omegaconf/grammar_visitor.py b/omegaconf/grammar_visitor.py index c599244cd..efd4002cc 100644 --- a/omegaconf/grammar_visitor.py +++ b/omegaconf/grammar_visitor.py @@ -78,7 +78,7 @@ def defaultResult(self) -> List[Any]: def visitConfigKey(self, ctx: OmegaConfGrammarParser.ConfigKeyContext) -> str: from ._utils import _get_value - # interpolation | ID | LIST_INDEX + # interpolation | ID | INTER_KEY assert ctx.getChildCount() == 1 child = ctx.getChild(0) if isinstance(child, OmegaConfGrammarParser.InterpolationContext): diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index d748e4325..ef2bb2495 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -571,6 +571,32 @@ def test_supported_chars() -> None: assert c.dir1 == supported_chars +def test_valid_key_names() -> None: + invalid_chars = "\\${}()[].: '\"" + valid_chars = "".join(chr(i) for i in range(33, 128) if chr(i) not in invalid_chars) + cfg_dict = {valid_chars: 123, "inter": f"${{{valid_chars}}}"} + cfg = OmegaConf.create(cfg_dict) + # Test that we can access the node made of all valid characters, both + # directly and through interpolations. + assert cfg[valid_chars] == 123 + assert cfg.inter == 123 + # Test that all invalid characters trigger errors in interpolations. + for c in invalid_chars: + cfg_dict["invalid"] = f"${{ab{c}de}}" + cfg = OmegaConf.create(cfg_dict) + error: type + if c in [".", "}"]: + # With '.', we try to access `${ab.de}`. + # With "}", we try to access `${ab}`. + error = ConfigKeyError + elif c == ":": + error = UnsupportedInterpolationType # `${ab:de}` + else: + error = GrammarParseError # other cases are all parse errors + with pytest.raises(error): + cfg.invalid + + def test_interpolation_in_list_key_error() -> None: # Test that a KeyError is thrown if an str_interpolation key is not available c = OmegaConf.create(["${10}"]) @@ -655,6 +681,7 @@ def _maybe_create(definition: str) -> Any: ("None", {"True": 1}, ...), # used to test keys with null-like names ("0", 42, ...), # used to test keys with int names ("1", {"2": 1337}, ...), # used to test dot-path with int keys + ("x@y", 123, ...), # Special keywords. ("null", "${test:null}", None), ("true", "${test:TrUe}", True), @@ -697,7 +724,7 @@ def _maybe_create(definition: str) -> Any: ("list_access_1", "${prim_list.0}", -1), ("list_access_2", "${test:${prim_list.1},${prim_list.2}}", ["a", 1.1]), ("list_access_underscore", "${prim_list.1_000}", ConfigKeyError), # "working" - ("list_access_bad_negative", "${prim_list.-1}", GrammarParseError), + ("list_access_bad_negative", "${prim_list.-1}", ConfigKeyError), ("dict_access_list_like_1", "${0}", 42), ("dict_access_list_like_2", "${1.2}", 1337), ("bool_like_keys", "${FalsE.TruE}", True), @@ -706,6 +733,7 @@ def _maybe_create(definition: str) -> Any: ("null_like_key_quoted_1", "${'None'.'True'}", GrammarParseError), ("null_like_key_quoted_2", "${'None.True'}", GrammarParseError), ("dotpath_bad_type", "${prim_dict.${float}}", GrammarParseError), + ("at_in_key", "${x@y}", 123), # Resolver interpolations. ("no_args", "${test:}", []), ("space_in_args", "${test:a, b c}", ["a", "b c"]), @@ -716,6 +744,7 @@ def _maybe_create(definition: str) -> Any: ("dict_list_as_key", "${test:{[0]: 1}}", GrammarParseError), ("missing_resolver", "${MiSsInG_ReSoLvEr:0}", UnsupportedInterpolationType), ("non_str_resolver", "${${bool}:}", GrammarParseError), + ("at_in_resolver", "${y@z:}", GrammarParseError), # Env resolver (limited: more tests in `test_env_values_are_typed()`). ("env_int", "${env:OMEGACONF_TEST_ENV_INT}", 123), ("env_missing_str", "${env:OMEGACONF_TEST_MISSING,miss}", "miss"),