Skip to content

Commit

Permalink
Grammar support for interpolations like ${.}. ${..} etc. (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
omry committed Mar 12, 2021
1 parent f056ee6 commit 23785a7
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 13 deletions.
2 changes: 1 addition & 1 deletion omegaconf/grammar/OmegaConfGrammarParser.g4
Expand Up @@ -41,7 +41,7 @@ sequence: (element (COMMA element?)*) | (COMMA element?)+;
// Interpolations.

interpolation: interpolationNode | interpolationResolver;
interpolationNode: INTER_OPEN DOT* configKey (DOT configKey)* INTER_CLOSE;
interpolationNode: INTER_OPEN DOT* (configKey (DOT configKey)*)? INTER_CLOSE;
interpolationResolver: INTER_OPEN resolverName COLON sequence? BRACE_CLOSE;
configKey: interpolation | ID | INTER_KEY;
resolverName: (interpolation | ID) (DOT (interpolation | ID))* ; // oc.env, myfunc, ns.${x}, ns1.ns2.f
Expand Down
2 changes: 1 addition & 1 deletion omegaconf/grammar_parser.py
Expand Up @@ -19,7 +19,7 @@
# Build regex pattern to efficiently identify typical interpolations.
# See test `test_match_simple_interpolation_pattern` for examples.
_id = "[a-zA-Z_]\\w*" # foo, foo_bar, abc123
_dot_path = f"{_id}(\\.{_id})*" # foo, foo.bar3, foo_.b4r.b0z
_dot_path = f"(\\.)*({_id}(\\.{_id})*)?" # foo, foo.bar3, foo_.b4r.b0z
_inter_node = f"\\${{\\s*{_dot_path}\\s*}}" # node interpolation
_arg = "[a-zA-Z_0-9/\\-\\+.$%*@]+" # string representing a resolver argument
_args = f"{_arg}(\\s*,\\s*{_arg})*" # list of resolver arguments
Expand Down
4 changes: 2 additions & 2 deletions omegaconf/grammar_visitor.py
Expand Up @@ -146,8 +146,8 @@ def visitInterpolation(
def visitInterpolationNode(
self, ctx: OmegaConfGrammarParser.InterpolationNodeContext
) -> Optional["Node"]:
# INTER_OPEN DOT* configKey (DOT configKey)* INTER_CLOSE
assert ctx.getChildCount() >= 3
# INTER_OPEN DOT* (configKey (DOT configKey)*)? INTER_CLOSE
assert ctx.getChildCount() >= 2

inter_key_tokens = [] # parsed elements of the dot path
for child in ctx.getChildren():
Expand Down
89 changes: 89 additions & 0 deletions tests/test_grammar.py
Expand Up @@ -6,6 +6,7 @@
from pytest import mark, param, raises, warns

from omegaconf import (
Container,
DictConfig,
ListConfig,
OmegaConf,
Expand Down Expand Up @@ -523,6 +524,13 @@ def visit() -> Any:
"${foo:bar,0,a-b+c*d/$.%@}",
"\\${foo}",
"${foo.bar:boz}",
# relative interpolations
"${.}",
"${..}",
"${..foo}",
"${..foo.bar}",
# config root
"${}",
],
)
def test_match_simple_interpolation_pattern(expression: str) -> None:
Expand All @@ -545,3 +553,84 @@ def test_match_simple_interpolation_pattern(expression: str) -> None:
)
def test_do_not_match_simple_interpolation_pattern(expression: str) -> None:
assert grammar_parser.SIMPLE_INTERPOLATION_PATTERN.match(expression) is None


def test_empty_stack() -> None:
"""
Check that an empty stack during ANTLR parsing raises a `GrammarParseError`.
"""
with raises(GrammarParseError):
grammar_parser.parse("ab}", lexer_mode="VALUE_MODE")


@mark.parametrize(
("inter", "key", "expected"),
[
# config root
param(
"${}",
"",
{"dict": {"bar": 20}, "list": [1, 2]},
id="absolute_config_root",
),
# simple
param("${dict.bar}", "", 20, id="dict_value"),
param("${dict}", "", {"bar": 20}, id="dict_node"),
param("${list}", "", [1, 2], id="list_node"),
param("${list.0}", "", 1, id="list_value"),
# relative
param(
"${.}",
"",
{"dict": {"bar": 20}, "list": [1, 2]},
id="relative:root_from_root",
),
param(
"${.}",
"dict",
{"bar": 20},
id="relative:root_from_dict",
),
param(
"${..}",
"dict",
{"dict": {"bar": 20}, "list": [1, 2]},
id="relative:parent_from_dict",
),
param(
"${..list}",
"dict",
[1, 2],
id="relative:list_from_dict",
),
param("${..list.1}", "dict", 2, id="up_down"),
],
)
def test_parse_interpolation(inter: Any, key: Any, expected: Any) -> None:
cfg = OmegaConf.create(
{
"dict": {"bar": 20},
"list": [1, 2],
},
)

root = OmegaConf.select(cfg, key)

tree = grammar_parser.parse(
parser_rule="singleElement",
value=inter,
lexer_mode="VALUE_MODE",
)

def callback(inter_key: Any) -> Any:
assert isinstance(root, Container)
ret = root._resolve_node_interpolation(inter_key=inter_key)
return ret

visitor = grammar_visitor.GrammarVisitor(
node_interpolation_callback=callback,
resolver_interpolation_callback=None, # type: ignore
quoted_string_callback=lambda s: s,
)
ret = visitor.visit(tree)
assert ret == expected
9 changes: 0 additions & 9 deletions tests/test_interpolation.py
Expand Up @@ -15,7 +15,6 @@
OmegaConf,
Resolver,
ValidationError,
grammar_parser,
)
from omegaconf._utils import _ensure_container
from omegaconf.errors import (
Expand Down Expand Up @@ -713,14 +712,6 @@ def test_optional_after_interpolation() -> None:
cfg.opt_num = None


def test_empty_stack() -> None:
"""
Check that an empty stack during ANTLR parsing raises a `GrammarParseError`.
"""
with pytest.raises(GrammarParseError):
grammar_parser.parse("ab}", lexer_mode="VALUE_MODE")


@pytest.mark.parametrize("ref", ["missing", "invalid"])
def test_invalid_intermediate_result_when_not_throwing(
ref: str, restore_resolvers: Any
Expand Down

0 comments on commit 23785a7

Please sign in to comment.