Skip to content
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

Merged
merged 17 commits into from
Aug 29, 2020

Conversation

pereman2
Copy link
Contributor

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.

@pereman2 pereman2 changed the title Bug/valuenodes Fix bug that allowed Containers to be set in unwished ValueNodes. Aug 20, 2020
@omry omry changed the title Fix bug that allowed Containers to be set in unwished ValueNodes. Fix bug that allowed containers to be set as a value in ValueNodes Aug 20, 2020
omegaconf/basecontainer.py Outdated Show resolved Hide resolved
tests/structured_conf/test_structured_config.py Outdated Show resolved Hide resolved
tests/test_errors.py Outdated Show resolved Hide resolved
news/324.bugfix Outdated Show resolved Hide resolved
omegaconf/basecontainer.py Show resolved Hide resolved
omegaconf/nodes.py Outdated Show resolved Hide resolved
omegaconf/basecontainer.py Outdated Show resolved Hide resolved
return is_dict_container(obj) or is_list_container(obj)


def is_list_container(obj: Any) -> bool:
Copy link
Owner

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}) 

Comment on lines 441 to 442
def is_container(obj: Any) -> bool:
return is_dict_container(obj) or is_list_container(obj)
Copy link
Owner

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.

@@ -177,7 +177,7 @@ def __init__(
)

def validate_and_convert(self, value: Any) -> Optional[str]:
if is_primitive_container(value):
if is_container(value):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if is_container(value):
from omegaconf import OmegaConf
if OmegaConf.is_config(value) or utils.is_primitive_container(value):

omegaconf/basecontainer.py Outdated Show resolved Hide resolved
Comment on lines 401 to 402
# if target_node is a valuenode and is not an anynode then
# it should set value
Copy link
Owner

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?

Copy link
Contributor Author

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?

Copy link
Owner

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.

Copy link
Contributor Author

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?

Copy link
Owner

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?

Copy link
Contributor Author

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.

Copy link
Owner

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.

Copy link
Contributor Author

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.

Comment on lines 396 to 397
# Target node is a container and has an special value or has an
# ref_type assigned
Copy link
Owner

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.

Comment on lines -440 to +450
self.__dict__["_content"][key]._set_value(value)
self.__dict__["_content"][key] = wrap(key, value)
Copy link
Owner

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.

Comment on lines 180 to 185
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)"
)
Copy link
Owner

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).

Copy link
Owner

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.

Copy link
Contributor Author

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/basecontainer.py Outdated Show resolved Hide resolved
omegaconf/basecontainer.py Show resolved Hide resolved
omegaconf/basecontainer.py Outdated Show resolved Hide resolved
tests/test_basic_ops_dict.py Outdated Show resolved Hide resolved
Comment on lines 180 to 185
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)"
)
Copy link
Owner

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.

omegaconf/basecontainer.py Show resolved Hide resolved
tests/test_nodes.py Show resolved Hide resolved
@@ -74,6 +74,10 @@ def test_valid_inputs(type_: type, input_: Any, output_: Any) -> None:
assert str(node) == str(output_)


class Person:
Copy link
Owner

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"

Copy link
Owner

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.

with pytest.raises(ValidationError):
type_(input_)
if type_ == AnyNode:
with pytest.raises(UnsupportedValueType):
Copy link
Owner

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Owner

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.

@pereman2
Copy link
Contributor Author

I rebased incorrectly i'll re-commit.

@lgtm-com
Copy link

lgtm-com bot commented Aug 27, 2020

This pull request introduces 2 alerts when merging 0cdb64f into 2579bd8 - view on LGTM.com

new alerts:

  • 1 for Unused import
  • 1 for Syntax error

@@ -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
Copy link
Owner

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

@pereman2 pereman2 Aug 27, 2020

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

Copy link
Owner

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.

Copy link
Owner

@omry omry left a 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.

@@ -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
Copy link
Owner

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]),
Copy link
Owner

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] ?

Copy link
Contributor Author

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

Copy link
Owner

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.

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)",
Copy link
Owner

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]'

@@ -74,6 +74,10 @@ def test_valid_inputs(type_: type, input_: Any, output_: Any) -> None:
assert str(node) == str(output_)


class Person:
Copy link
Owner

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.

with pytest.raises(ValidationError):
type_(input_)
if type_ == AnyNode:
with pytest.raises(UnsupportedValueType):
Copy link
Owner

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.

Copy link
Owner

@omry omry left a 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]),
Copy link
Owner

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.

@omry omry merged commit 647c1c7 into omry:master Aug 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ValueNodes are not validating correctly.
2 participants