-
Notifications
You must be signed in to change notification settings - Fork 121
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
Fix bug that allowed containers to be set as a value in ValueNodes #335
Conversation
omegaconf/_utils.py
Outdated
return is_dict_container(obj) or is_list_container(obj) | ||
|
||
|
||
def is_list_container(obj: Any) -> bool: |
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.
this would be true, and it's clearly not a list container.
is_list_container(OmegaConf.create({"foo": 10})
omegaconf/_utils.py
Outdated
def is_container(obj: Any) -> bool: | ||
return is_dict_container(obj) or is_list_container(obj) |
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.
You already have OmegaConf.is_config() and utils.is_primitive_container().
Use those writing so many new functions.
omegaconf/nodes.py
Outdated
@@ -177,7 +177,7 @@ def __init__( | |||
) | |||
|
|||
def validate_and_convert(self, value: Any) -> Optional[str]: | |||
if is_primitive_container(value): | |||
if is_container(value): |
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.
if is_container(value): | |
from omegaconf import OmegaConf | |
if OmegaConf.is_config(value) or utils.is_primitive_container(value): |
omegaconf/basecontainer.py
Outdated
# if target_node is a valuenode and is not an anynode then | ||
# it should set value |
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.
why is AnyNode different than other nodes in this context?
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.
Because AnyNode could be anything from primitive to container?
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.
You are not supposed to put containers in AnyNode, containers are ALWAYS converted to DictConfig or ListConfig.
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.
Then if you make a config cfg = OmegaConf.create({"foo": 4})
and cfg.foo = []
it should wrap a new ListConfig. But if instead the config is:
@dataclass
class C:
foo: Any = 4
cfg= OmegaConf.structured(C)
cfg.foo = []
it should give an error?
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.
No, it should do the same in both cases.
Why would the second one give an error?
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.
Because AnyNode doesn't allow containers.
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.
As I said, that container (dict or list) should be wrapped in a DictConfig or a ListConfig and REPLACE the AnyNode.
It can do that because the ref_type allows it.
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.
Then an AnyNode will never set its value it will always wrap a new node even if it's a primitive type. ref_type=Any is the default so it should never be a problem wrapping always.
omegaconf/basecontainer.py
Outdated
# Target node is a container and has an special value or has an | ||
# ref_type assigned |
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.
so this comment does not really explain the reason for needing a set.
That sentence does not make sense to me.
a sentence that would make sense:
# We use set_value if:
# 1. input is a primitive and the value is a leaf node
# 2. Input is anything any the target is MISSING or None
# 3. ...
Can you maybe explain it in terms of when do we NOT use set_value?
Don't explain the code ,explain the reasons behind it.
I know this is hard, but I think it will lead to much better code here overall.
self.__dict__["_content"][key]._set_value(value) | ||
self.__dict__["_content"][key] = wrap(key, value) |
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.
see, the fact you can do that means that wrap is actually not wrap. (indeed internally it calls maybe wrap.
it feels like there is an opportunity for a significant cleanup of this function.
However, this does not need to happen in this diff.
Let's try to at least clarify what we are doing here and why. Actual changes can happen later.
omegaconf/nodes.py
Outdated
from omegaconf import OmegaConf | ||
|
||
if OmegaConf.is_config(value) or is_primitive_container(value): | ||
raise ValidationError( | ||
"Value '$VALUE' is not a valid string (type $VALUE_TYPE)" | ||
) |
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.
AnyNode should also reject containers (+ tests).
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.
did you address this? re-reviewing the entire diff looking for things like this is time consuming.
if you did not address this please do, if you did please clarify it here.
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.
It rejects them with the current implementation, line149: if not is_primitive_type(value):
omegaconf/nodes.py
Outdated
from omegaconf import OmegaConf | ||
|
||
if OmegaConf.is_config(value) or is_primitive_container(value): | ||
raise ValidationError( | ||
"Value '$VALUE' is not a valid string (type $VALUE_TYPE)" | ||
) |
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.
did you address this? re-reviewing the entire diff looking for things like this is time consuming.
if you did not address this please do, if you did please clarify it here.
tests/test_nodes.py
Outdated
@@ -74,6 +74,10 @@ def test_valid_inputs(type_: type, input_: Any, output_: Any) -> None: | |||
assert str(node) == str(output_) | |||
|
|||
|
|||
class Person: |
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.
There is already a class we can use for this, look for "IllegalType"
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.
remove class Person
and use IllegalType
instead.
tests/test_nodes.py
Outdated
with pytest.raises(ValidationError): | ||
type_(input_) | ||
if type_ == AnyNode: | ||
with pytest.raises(UnsupportedValueType): |
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.
didn't we say we can make UnsupportedValueType a subclass of ValidationError?
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.
Ah I thought you meant we could make them on 2.1
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.
I misunderstood you, I'll change it.
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.
You don't need to special-case AnyNode now that the Exception is a ValidationError.
I rebased incorrectly i'll re-commit. |
This pull request introduces 2 alerts when merging 0cdb64f into 2579bd8 - view on LGTM.com new alerts:
|
0cdb64f
to
bc7f32c
Compare
tests/test_basic_ops_dict.py
Outdated
@@ -539,17 +539,39 @@ def test_set_with_invalid_key() -> None: | |||
cfg[1] = "a" # type: ignore | |||
|
|||
|
|||
def test_set_anynode() -> None: | |||
@pytest.mark.parametrize("value", [1, 3.14, True, None, Enum1.FOO]) # type: ignore |
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.
these tests should probably be in test_nodes.py
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.
Well this one tests if the node is not replaced. That cannot be done in test_nodes.
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.
But yeah, I missed adding these testcases in test_valid_inputs in test_nodes
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.
It's okay. Please move the tests to test_nodes.
876f26f
to
0f5cd66
Compare
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.
I am ready to release Hydra 1.0 and I am going to release OmegaConf 2.0.1 as well.
Can you finish your two pending pull requests during the weekend?
If not I can take over.
tests/test_basic_ops_dict.py
Outdated
@@ -539,17 +539,39 @@ def test_set_with_invalid_key() -> None: | |||
cfg[1] = "a" # type: ignore | |||
|
|||
|
|||
def test_set_anynode() -> None: | |||
@pytest.mark.parametrize("value", [1, 3.14, True, None, Enum1.FOO]) # type: ignore |
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.
It's okay. Please move the tests to test_nodes.
pytest.param( | ||
Expected( | ||
create=lambda: OmegaConf.structured(User), | ||
op=lambda cfg: cfg.__setitem__("name", [1, 2]), |
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.
Why not cfg.name = [1,2]
?
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.
Lambda doesn't allow that, and every other test uses setitem
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.
oh, right. forgot about that shit.
tests/test_errors.py
Outdated
create=lambda: OmegaConf.structured(User), | ||
op=lambda cfg: cfg.__setitem__("name", [1, 2]), | ||
exception_type=ValidationError, | ||
msg="Value '[1, 2]' is not a valid string (type list)", |
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.
This is a better error:
Cannot convert 'list' to string : '[1, 2]'
tests/test_nodes.py
Outdated
@@ -74,6 +74,10 @@ def test_valid_inputs(type_: type, input_: Any, output_: Any) -> None: | |||
assert str(node) == str(output_) | |||
|
|||
|
|||
class Person: |
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.
remove class Person
and use IllegalType
instead.
tests/test_nodes.py
Outdated
with pytest.raises(ValidationError): | ||
type_(input_) | ||
if type_ == AnyNode: | ||
with pytest.raises(UnsupportedValueType): |
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.
You don't need to special-case AnyNode now that the Exception is a ValidationError.
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.
Awesome!
pytest.param( | ||
Expected( | ||
create=lambda: OmegaConf.structured(User), | ||
op=lambda cfg: cfg.__setitem__("name", [1, 2]), |
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.
oh, right. forgot about that shit.
Closes #324 .
I added tests but I think maybe I should remove the one in test_structured_config since is essentially the same as test_errors.
The variable should_set_value is getting pretty big but this fixed it.