-
Notifications
You must be signed in to change notification settings - Fork 106
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
Dict getitem consistency #516
Conversation
@@ -372,7 +372,8 @@ def _get_node( | |||
self, | |||
key: Union[int, slice], | |||
validate_access: bool = True, | |||
throw_on_missing: bool = False, | |||
throw_on_missing_value: bool = False, | |||
throw_on_missing_key: bool = False, |
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.
Currently ListConfig._get_node
gives the same behavior whether throw_on_missing_key
is True
or False
(because the method does not make any use of this keyword argument).
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.
Yes, this is coming from the interface. there was no need yet to support it.
This is unrelated to this PR though, feel free to post a followup issue fixing it (there aren't a lot of uses of throw_on_missing yet, we may eventually just revert it is there isn't any need).
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 agree it should be fixed, as there's currently an inconsistency between dicts and lists, because dict._get_node(foo)
will return None
if the key is missing, while list._get_node(foo)
will crash if the index is invalid.
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.
_get_node is an internal API. As long as there is no external API that can expose an issue because of it I don't think it's particularly important.
The behavior of _get_node() in both cases is driven by the needs of OmegaConf. consistency between ListConfig and DictConfig is not an objective.
1e66064
to
1e4cc3f
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.
A few comments
@@ -372,7 +372,8 @@ def _get_node( | |||
self, | |||
key: Union[int, slice], | |||
validate_access: bool = True, | |||
throw_on_missing: bool = False, | |||
throw_on_missing_value: bool = False, | |||
throw_on_missing_key: bool = False, |
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 agree it should be fixed, as there's currently an inconsistency between dicts and lists, because dict._get_node(foo)
will return None
if the key is missing, while list._get_node(foo)
will crash if the index is invalid.
@@ -196,7 +196,7 @@ def finalize(self, cfg: Any) -> None: | |||
Expected( | |||
create=lambda: OmegaConf.create({"foo": "${missing}"}), | |||
op=lambda cfg: getattr(cfg, "foo"), | |||
exception_type=ConfigKeyError, | |||
exception_type=ConfigAttributeError, |
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 wonder if missing interpolations should have their own exception? (that would neither be a KeyError nor an AttributeError)
Reason is that people may use exception-based programming with the new API, to detect missing keys:
try:
foo = cfg.foo
except AttributeError:
foo = cfg.foo = 42
print(foo)
and if the AttributeError
is raised because foo
is referring to a missing interpolation, then bad things may happen.
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 that bad?
you accessed a field, it was pointing to something that is not there. you assign an actual value to it. It's no longer a pointer but a real value. underlying pointed to field is unchanged.
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.
Someone writing such code most likely only intended to override cfg.foo
if there was no such key in the config already. This could lead to subtle bugs if suddenly an interpolation gets broken (e.g., because a variable was renamed in the config), because instead of seeing an error the code will run just fine (ignoring the broken interpolation).
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.
Create a concrete example demonstrating a problem.
writing values on an interpolation is not writing through the interpolation.
Also, your example will not even work now because the exception raised is not an ConfigAttributeError but a ConfigKeyError right now:
cfg = OmegaConf.create({"a" : "${b}"})
cfg.a # ConfigKeyError
cfg["a"] # ConfigKeyError
and in any case writing on an interpolation is not write through:
In [11]: cfg
Out[11]: {'a': '${b}'}
In [12]: cfg.a = 10
In [13]: cfg
Out[13]: {'a': 10}
I am open to changes here but it's a different topic (and does not much much to do with standardizing with dict and object (given that they do not support interpolations).
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.
Are you on master? I'm at d018678 and in your example cfg.a
raises a ConfigAttributeError
for me.
Here's an example illustrating what I have in mind: note how the typo in the interpolation makes it being silently ignored.
cfg = OmegaConf.create("""
default_ram: 32
super_computer_ram: 128
machines: [m1, m2, m3, m4, m5]
ram_per_machine:
m3: 64
m5: ${super_computerr_ram}
""")
# Ensure `cfg.ram_per_machine` contains RAM data for all machines.
ram_per_machine = cfg.ram_per_machine
for machine in cfg.machines:
try:
ram = getattr(ram_per_machine, machine)
except AttributeError:
ram = cfg.default_ram
setattr(ram_per_machine, machine, cfg.default_ram)
print(f"RAM for {machine}: {ram}")
print("\nConfig:\n")
print(OmegaConf.to_yaml(cfg, resolve=True))
The output:
RAM for m1: 32
RAM for m2: 32
RAM for m3: 64
RAM for m4: 32
RAM for m5: 32
Config:
default_ram: 32
super_computer_ram: 128
machines:
- m1
- m2
- m3
- m4
- m5
ram_per_machine:
m3: 64
m5: 32
m1: 32
m2: 32
m4: 32
It's not a huge deal, I just thought I'd point it out (it wasn't a concern before because there were no exceptions to catch)
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 wonder if missing interpolations should have their own exception? (that would neither be a KeyError nor an AttributeError)
So the idea is that each of the following would raise a different type of exception:
OmegaConf.create("").a # raises ConfigAttributeError
OmegaConf.create("a: ${xyz}").a # also raises ConfigAttributeError
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 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.
@Jasha10 said it better than I could have (thank you!)
:) my pleasure.
We already have
class InterpolationResolutionError(OmegaConfBaseException, ValueError):
defined in omegaconf.errors
. Why not throw that one?
On the master
branch, this InterpolationResolutionError
is not being used anywhere else in the codebase.
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.
Ha, looks like I added it when I added relative interpolations but somehow never used it.
I am fine with using it to indicate an that an interpolation could not be resolved.
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've opened a PR (#523) to use InterpolationResolutionError
for this purpose.
DictConfig __getitem__ is now constent with plain dict (raises a KeyError if a key is not found instead of returning None) DictConfig __getattr__ is now consistent with plain objects (raises an AttributeError if an attribute is not found instead of returning None).
2add1c9
to
96cc641
Compare
Fixes #515.
This is an historical mistake. See #515 for some explanation and migration instructions.