-
Notifications
You must be signed in to change notification settings - Fork 111
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
Consider supporting basic operators in interpolations of numbers #91
Comments
+1. Out of curiosity, why not use the default behavior of pythons operands for any types? I.e. don't check the type (let python return its errors if need be) and just call + , - , / , * . For example do string concatenation by default with +. |
String concatenation is already supported directly: url: http://${host}:${port}:${path} It might still be worth supporting it, but some other things does not make immediate sense, for example: a:
bb: 10
cb: 20
b:
xx: 30
yy: 40
c: foo
d1: ${a} + ${b}
d2: ${a} + ${c} While it's plausible to come with interpretations for both d1 and maybe even d2, I suspect the surface area from supporting those interpretation is very large. |
Ok, I would personally just use python way of doing by returning def interpolate_binary(a,b,op):
return op(OmegaConf.to_container(a, resolve=True),
OmegaConf.to_container(b, resolve=True)) In which case both d1 and d2 would raise a TypeError. That seems like the most intuitive and easiest way of maintaining things. But maybe it's naive, I didn't actually look through OmegaConf codebase much ... |
Supporting + for arbitrary values may be incompatible with existing strings: foo: FOO
bar: BAR
key: ${foo}+${bar} current interpretation is There should be a way to prevent operators from executing for cases like this. In any case, when I try to actually implement it I will think about it more seriously. |
@odelalleau, two : #{1 * 2}
five : #{1 + ${two} * 2}
# or using a regular resolver:
eight : ${eval:4 + ${two} * 2} I kinda like #{} though (although I just realized it's a yaml comment so it will have unfortunate side effects. some other symbol? |
Personally I would find it more intuitive to do it the way you initially wrote at the top of this issue, rather than introduce a new resolver or syntax. Did you have any major concern with your initial proposition? (I think it should remain limited to floats & integers, which should cover most common use cases, and people can still write custom resolvers for more advanced computations). |
The main concern is backward compatibility. x: 10 * 2 Quoting it will not help because at YAML level it's already a string. # "10 * 2"
x: 10 * 2
# 20
y: ${eval:10 * 2} It is possible to only process expressions if they contain interpolations, but feels like a hack.
I guess we could go with the new decode resolver to support the string form:
|
Follow-up thoughts on this (tl;dr: I think it can be implemented at the level of resolver arguments, but this would make it a bit more cumbersome to use arithmetic operators in strings especially on Hydra's CLI, so maybe a unique syntax like
|
Makes sense. I don't think we need to deprecate anything at this time (since we are not sure about the final design yet). |
A few thoughts here
from dataclasses import dataclass
import hydra
from hydra.core.config_store import ConfigStore
from omegaconf import OmegaConf
@dataclass
class Conf:
foo: str = 'bar'
baz: int = "${eval: 1 if ${foo} == 'bar' else 0}" # Magic, yet-to-be-implemented resolver
cs = ConfigStore.instance()
cs.store('config', Conf)
@hydra.main(config_name='config', config_path=None)
def main(cfg):
resolved_cfg = OmegaConf.to_container(cfg, resolve=True, enum_to_str=True)
print(OmegaConf.to_yaml(resolved_cfg))
if __name__ == '__main__':
main() It'd be cool to have this output
Currently to get the equivalent behavior, you need to introduce some auxiliary resolvers def resolve_if(condition, true_value, false_value):
return true_value if condition else false_value
def resolve_eq(first, second):
return first == second
@dataclass
class Conf:
foo: str = 'bar'
baz: int = "${if: ${eq: ${foo}, 'bar'}, 1, 0}"
OmegaConf.register_new_resolver('if', resolve_if)
OmegaConf.register_new_resolver('eq', resolve_eq) Personally I find the
|
Hopefully xD |
I'd like to remark that using python's from typing import Any
from omegaconf import OmegaConf
OmegaConf.register_new_resolver("eval", eval)
cfg = OmegaConf.create('a: "Computed ${eval: 2 * 3 * 5}"')
print(cfg.a) # Prints 'Computed 30' Using nested interpolations is a little bit tricky, but careful quoting makes it possible: cfg = OmegaConf.create(
"""
foo: bar
baz: ${eval:'1 if "${foo}" == "bar" else 0'}
"""
)
print(cfg.baz) # Prints '1' The interpolation |
@Jasha10 That's really cool! Ideally I would want there to be a syntax that works in all cases, but the spaces and quotes really make things quite tricky. After importing/registering, I ran the following tests: # Test A: Quote the entire RHS, with a space after `eval:`
def test_1a_eval_outer_with_space():
cfg = OmegaConf.create("""
a: '${eval: 2 * 3 * 5}'
""")
assert cfg.a == 30 # => pass!
def test_2a_nested_outer_with_space():
cfg = OmegaConf.create("""
foo: bar
baz: '${eval: 1 if "${foo}" == "bar" else 0}'
""") # => GrammarParseError: token recognition error at: '='
assert cfg.baz == 1
# --------------------------------------------------------------------
# Test B: Quote the insides only, with a space after `eval:`
def test_1b_eval_inner_with_space():
cfg = OmegaConf.create("""
a: ${eval: '2 * 3 * 5'}
""") # ^ => ScannerError: mapping values are not allowed here
assert cfg.a == 30
def test_2b_nested_inner_with_space():
cfg = OmegaConf.create("""
foo: bar
baz: ${eval: '1 if "${foo}" == "bar" else 0'}
""") # ^ => ScannerError: mapping values are not allowed here
assert cfg.baz == 1
# --------------------------------------------------------------------
# Test C: Quote entire RHS, with no space after `eval:`
def test_1c_eval_outer_no_space():
cfg = OmegaConf.create("""
a: '${eval:2 * 3 * 5}'
""")
assert cfg.a == 30 # => pass!
def test_2c_nested_outer_no_space():
cfg = OmegaConf.create("""
foo: bar
baz: '${eval:1 if "${foo}" == "bar" else 0}'
""") # => GrammarParseError: mismatched input '"' expecting BRACE_CLOSE
assert cfg.baz == 1
# --------------------------------------------------------------------
# Test D: Quote the insides only, with no space after `eval:`
def test_1d_eval_inner_no_space():
cfg = OmegaConf.create("""
a: ${eval:'2 * 3 * 5'}
""")
assert cfg.a == 30 # => pass!
def test_2d_nested_inner_no_space():
cfg = OmegaConf.create("""
foo: bar
baz: ${eval:'1 if "${foo}" == "bar" else 0'}
""")
assert cfg.baz == 1 # => pass!
# -------------------------------------------------------------------- The best option seems to be method D, immediately following But I don't have a clue as to why... |
If you don't quote the string then you are restricted to only characters allowed in unquoted strings, so things will break as soon as you use another character in your expression. For instance, quotes are not allowed, which explains some of your errors. Putting a space after Here are two ways to fix it (besides removing the space): # Dropping YAML
def test_2b_nested_inner_with_space():
cfg = OmegaConf.create(
{
"foo": "bar",
"baz": """${eval: '1 if "${foo}" == "bar" else 0'}"""
}
)
assert cfg.baz == 1 # Quoting the YAML string (which requires escaping the inner quotes for the YAML parser)
# (note that in a .yaml file you would use a single \, here it is doubled because it is in a Python string)
def test_2b_nested_inner_with_space():
cfg = OmegaConf.create("""
foo: bar
baz: "${eval: '1 if \\"${foo}\\" == \\"bar\\" else 0'}"
""")
assert cfg.baz == 1 |
Thanks, that's helpful. That also explains why the |
But I have not understood how in this syntax how you can write things like:
The above should work but it doesn't in my case :( |
It's probably just a YAML parsing error, you can fix it by removing the space after |
I did this and now I get this: |
Hi @ysig, see the OmegaConf docs on How to Perform Arithmetic Using eval as a Resolver. |
I have seen them but I don't see where my error is. After all I just use
multiplication.
…On Tue, Dec 20, 2022, 14:15 Jasha Sommer-Simpson ***@***.***> wrote:
Hi @ysig <https://github.com/ysig>, see the OmegaConf docs on How to
Perform Arithmetic Using eval as a Resolver
<https://omegaconf.readthedocs.io/en/2.3_branch/how_to_guides.html#id1>
—
Reply to this email directly, view it on GitHub
<#91 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGY7H2OEOBIXVHNUU2UP25LWOGPPJANCNFSM4JTY26UA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
You are most likely forgetting to register the OmegaConf.register_new_resolver("eval", eval) |
Am I the only one concerned about the fact that registering It opens the door to arbitrary code execution via configuration, so I find it hard to believe that it is the suggested official workaround! Moreover, such a suggestion is IMHO also inconsistent with other Hydra design choices. Take for instance the Please don't get me wrong: I am a happy user of Hydra and I am thankful to all the contributors that work on this framework, I use the |
I agree it'd be good to add a warning, but IMO it's a useful tip that's worth keeping in the docs (there are many situations where people don't care about such a security risk -- I know I'm a happy user of the Regarding your comment on |
Opinions can differ of course, but IMO just adding a warning because it can be a useful tip is not enough given that we are talking about the official docs. For one person like you that is using it who is perfectly conscious of the risks, I bet there is another (and probably more) who will use it without thinking a moment about the risks because "it's in the docs". If I didn't know better, I would happily include it because it's so-damn-practical! I don't consider We are talking about a one-liner that makes the entire application unsecure, just its presence means that any parameter that is configurable by the end-user can be abused for code execution. Just imagine a multi-dev scenario where a developer added this resolver, and another one unaware of it exposes an innocuous parameter on the front-end... Since Hydra is a configuration framework, I really think this is where the docs should draw a line in the sand and absolutely discourage this. IMO, putting it in the docs means there is some blessing from the framework on using this pattern, even with a warning... Unless you make the warning really huge, but then why did you even include it in the docs? IMO such a suggestion does not belong to official docs, rather to a stackoverflow thread so that it can get up/downvoted and commented, with other safer alternatives discussed. The choice is down to Hydra developers of course, but personally, I think that a better solution would be either
My point was rather that this suggestion in the docs is inconsistent with the presence of other safer alternatives, not that |
@teocasse what if there existed a |
I cannot comment whether the specific implementation you suggest would be secure, but on a more general level I would not be against a "safer" eval implementation. With This post suggests a few alternatives to "plain eval" in order to parse mathematical expressions in a secure way: https://stackoverflow.com/questions/2371436/evaluating-a-mathematical-expression-in-a-string |
|
People don't expect to have to monitor configs for arbitrary code execution injection, and don't do it. |
desired behavior:
This should work only if both operands are numbers (int or float). result type should be int if both are ints or float otherwise.
EDIT (2022-12-20):
✨✨✨ See the OmegaConf docs on How to Perform Arithmetic Using eval as a Resolver ✨✨✨
The text was updated successfully, but these errors were encountered: