New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Grammar support for interpolations like ${.}. ${..} etc. #597
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dummy review because Github is having trouble registering my comments => hoping that closing this review will fix it
GitHub got issues now. |
Not sure, I'd say that the example you provided is clearer with explicitly passing
Yeah the boilerplate code ended up being more complex than I would have liked. Adding new tests should be straightorward though (once you understand how things work). The motivation was to be able to add a lot of tests with a minimum of extra "fluff" for each test. IMO this makes tests more readable (not the code, but the test data). There were no relative interpolation tests though it seems in that file (definitely an oversight) => right now tests with I don't mind changing things if you can be specific on the formatting you would like to see (I'd rather not take a guess and end up with something worse). Edit: oh, there was also another motivation, which is to just copy the test data into Hydra => it's easier if it's isolated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks!
The motivation is to support the use case of providing a default value for interpolation.
This is coming from #541, which is proposing a completely new syntax for it: a : 10
b : ${a, 20} # 20 if a is not there. I think a new syntax is an overkill and we should be able to leverage custom resolvers. The following is cleaner than forcing the ${.} everywhere and it seems just as flexible. v1: val1
foo:
v2 : val2
b: ${oc.select: v1, default} # val1
c: ${oc.select: .v2, default} # val2 In fact, this function is more Container.select() than OmegaConf.select().
Will respond to this part later. gtg. |
Got it. No objection from me as long as it remains optional. |
Would it be difficult to add support for going up multiple levels in the hierarchy? |
Like this? In [2]: cfg = OmegaConf.create({"v": 1, "a": {"b": {"c": "${...v}"}}})
In [3]: cfg
Out[3]: {'v': 1, 'a': {'b': {'c': '${...v}'}}}
In [4]: cfg.a.b.c
Out[4]: 1 |
Yes, exactly :) |
This in general is objectively a bit hard to test directly because we depend on a config object in the parsing.
Can you explain what is not supported with an example?
You can do this: interpolation_test_data = [
# config root
param(
"${}",
"",
{"dict": {"bar": 20}, "list": [1, 2]},
id="absolute_config_root",
),
# simple
param("${dict.bar}", "", 20, id="dict_value"),
...
]
@mark.parametrize(
("inter", "key", "expected"),
interpolation_test_data,
)
def test_parse_interpolation(inter: Any, key: Any, expected: Any) -> None:
... I think it's still valuable to have the test data close to the test itself. stashing it all at the top of the file makes it significantly harder to understand the tests. |
Don't try In [38]: cfg = OmegaConf.create({"v": 1, "a": "${}", "b": {"c" : "${..}"}})
In [39]: cfg
Out[39]: {'v': 1, 'a': '${}', 'b': {'c': '${..}'}}
In [40]: cfg.a
Out[40]: {'v': 1, 'a': '${}', 'b': {'c': '${..}'}}
In [41]: cfg.a.a.a
Out[41]: {'v': 1, 'a': '${}', 'b': {'c': '${..}'}}
In [42]: cfg.b.c
Out[42]: {'v': 1, 'a': '${}', 'b': {'c': '${..}'}}
# OmegaConf.to_container(cfg, resolve=True) # boom |
Right, I can definitely see how the current code is difficult to understand. However, once you know what's going on, you don't have to look at the code anymore and you can just add new test items in the corresponding list.
If I add the following item to the list ("rel_test", "${..foo}", None), then it will fail with with this traceback:
because the parent is the root config, so the parent of the parent doesn't exist. It'd be easy to fix by doing the resolution at a deeper level in the config.
Yeah, that's essentially what I did except that I got rid of the
I hear you. But that's kind of how it is in a sense: the way I see it, The exception is In terms of practical action item, I can easily move everything to Let me know whether to proceed with some of these changes (or others). |
What are you expecting
Can you file a followup PR?
I think we should use a single testing style in the codebase.
I have similar patterns in other places, like in test_errors.py (that I am not really happy with). Here is an example of a large test in Hydra. The test functions are usually the same and are calling a common generic function. Another problem I have with the current test is that the base config it's using to test has everything and it's mother, and it's defined far away from the test code.
I think a lot of the complexity now is that the test is trying to handle an arbitrary list of parameters in a generic way, even though the list looks different in each case. Things can probably be simplified by allowing each test to be more specialized (while reusing whatever is possible through some utility functions). All this is not particularly urgent, so no need to interrupt what you are working on. but I think we should add it to the queue. |
Oh, I was expecting
Probably not worth it at this point since you already added relative interpolation tests in this PR. Would probably make more sense to do it at the same time the tests would be refactored.
Ok will add it in my list somewhere. To be honest though I'm still not entirely sure how such splitting would look like. There aren't that many different things tested here, it's essentially all the same stuff (take an expression, parse it with the desired rule, check the result). But we can discuss it later when we get to it. |
What I did in Hydra, which makes a lot of sense there - is to test primitive rules separately. @mark.parametrize(
"value,expected",
[
param("[]", [], id="list:empty"),
param("[1]", [1], id="list:item"),
param(
"['a b']",
[QuotedString(text="a b", quote=Quote.single)],
id="list:quoted_item",
),
param(
"['[a,b]']",
[QuotedString(text="[a,b]", quote=Quote.single)],
id="list:quoted_item",
),
param("[[a]]", [["a"]], id="list:nested_list"),
param("[[[a]]]", [[["a"]]], id="list:double_nested_list"),
param("[1,[a]]", [1, ["a"]], id="list:simple_and_list_elements"),
],
)
def test_list_container(value: str, expected: Any) -> None:
ret = parse_rule(value, "listContainer")
assert ret == expected Not sure it's applicable here. |
I guess it could be applicable in the context of an individual config object. (that can have the minimum needed to test the specific rule). |
We could to some extent (that would essentially mean splitting
because if we only test |
I think it's reasonable to use the existing rules as is. |
Thinking about it a bit more, I think a better solution is to check that the whole string was processed. |
This enables implementing a form of select with a default value:
Side note, we should rethink support for passing the parent node automatically on demand (
${.}
), it will simplify this use case.Notes:
I did not like the complexity of the testing in test_grammar.py. seems like a new testing framework instead of a test designed to test some logic.
I created a simpler more localized test for interpolations. happy to discuss.
while it's not very big, it can be easier to review one commit at a time.