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

Added OmegaConf.resolve() #639

Merged
merged 8 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,31 @@ as DictConfig, allowing attribute style access on the resulting node.
>>> show(container["structured_config"])
type: DictConfig, value: {'port': 80, 'host': 'localhost'}

OmegaConf.resolve
^^^^^^^^^^^^^^^^^
.. code-block:: python

def resolve(cfg: Container) -> None:
"""
Resolves all interpolations in the given config object in-place.
:param cfg: An OmegaConf container (DictConfig, ListConfig)
Raises a ValueError if the input object is not an OmegaConf container.
"""

Normally interpolations are resolved lazily, at access time.
This function eagerly resolves all interpolations in the given config object in-place.
Example:

.. doctest::

>>> cfg = OmegaConf.create({"a": 10, "b": "${a}"})
>>> show(cfg)
type: DictConfig, value: {'a': 10, 'b': '${a}'}
>>> assert cfg.a == cfg.b == 10 # lazily resolving interpolation
>>> OmegaConf.resolve(cfg)
>>> show(cfg)
type: DictConfig, value: {'a': 10, 'b': 10}

OmegaConf.select
^^^^^^^^^^^^^^^^
OmegaConf.select() allow you to select a config node or value using a dot-notation key.
Expand Down
1 change: 1 addition & 0 deletions news/640.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds OmegaConf.resolve(cfg) for in-place interpolation resolution on cfg
44 changes: 44 additions & 0 deletions omegaconf/_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Any

from omegaconf import MISSING, Container, DictConfig, ListConfig, Node, ValueNode
from omegaconf.errors import InterpolationToMissingValueError


def _resolve_container_value(cfg: Container, node: Node, index: Any) -> None:
if node._is_interpolation():
try:
resolved = node._dereference_node()
assert resolved is not None
if isinstance(resolved, Container) and isinstance(node, ValueNode):
cfg[index] = resolved
else:
node._set_value(resolved._value())
except InterpolationToMissingValueError:
node._set_value(MISSING)
odelalleau marked this conversation as resolved.
Show resolved Hide resolved
else:
_resolve(node)


def _resolve(cfg: Node) -> Node:
assert isinstance(cfg, Node)
try:
if cfg._is_interpolation():
resolved = cfg._dereference_node()
assert resolved is not None
cfg._set_value(resolved._value())
except InterpolationToMissingValueError:
cfg._set_value(MISSING)
odelalleau marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(cfg, DictConfig):
for k in cfg.keys():
odelalleau marked this conversation as resolved.
Show resolved Hide resolved
node = cfg._get_node(k)
assert isinstance(node, Node)
_resolve_container_value(cfg, node, k)

elif isinstance(cfg, ListConfig):
for i in range(len(cfg)):
node = cfg._get_node(i)
assert isinstance(node, Node)
_resolve_container_value(cfg, node, i)
odelalleau marked this conversation as resolved.
Show resolved Hide resolved

return cfg
17 changes: 17 additions & 0 deletions omegaconf/omegaconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,23 @@ def to_yaml(cfg: Any, *, resolve: bool = False, sort_keys: bool = False) -> str:
Dumper=get_omega_conf_dumper(),
)

@staticmethod
def resolve(cfg: Container) -> None:
"""
Resolves all interpolations in the given config object in-place.
:param cfg: An OmegaConf container (DictConfig, ListConfig)
Raises a ValueError if the input object is not an OmegaConf container.
"""
import omegaconf._impl

if not OmegaConf.is_config(cfg):
# Since this function is mutating the input object in-place, it doesn't make sense to
# auto-convert the input object to an OmegaConf container
raise ValueError(
f"Invalid config type ({type(cfg).__name__}), expected an OmegaConf Container"
)
omegaconf._impl._resolve(cfg)

# === private === #

@staticmethod
Expand Down
99 changes: 99 additions & 0 deletions tests/test_omegaconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,102 @@ def test_is_issubclass() -> None:
cfg = OmegaConf.structured(ConcretePlugin)
t = OmegaConf.get_type(cfg)
assert t is not None and issubclass(t, ConcretePlugin)


@mark.parametrize(
("cfg", "expected"),
[
# dict
param(OmegaConf.create(), {}, id="dict"),
param(OmegaConf.create({"a": 10, "b": "${a}"}), {"a": 10, "b": 10}, id="dict"),
param(
OmegaConf.create({"a": 10, "b": {"a": "${a}"}}),
{"a": 10, "b": {"a": 10}},
id="dict",
),
param(
OmegaConf.create({"a": "${b.a}", "b": {"a": 10}}),
{"a": 10, "b": {"a": 10}},
id="dict",
),
param(OmegaConf.create({"a": "???"}), {"a": "???"}, id="dict:missing"),
param(
OmegaConf.create({"a": "???", "b": "${a}"}),
{"a": "???", "b": "???"},
id="dict:missing",
),
param(
OmegaConf.create({"a": 10, "b": "a_${a}"}),
{"a": 10, "b": "a_10"},
id="dict:str_inter",
),
# This seems like a reasonable resolution for a string interpolation pointing to a missing node:
omry marked this conversation as resolved.
Show resolved Hide resolved
param(
OmegaConf.create({"a": "???", "b": "a_${a}"}),
{"a": "???", "b": "???"},
id="dict:str_inter_missing",
),
param(
DictConfig("${a}", parent=OmegaConf.create({"a": {"b": 10}})),
{"b": 10},
id="inter_dict",
),
param(
DictConfig("${a}", parent=OmegaConf.create({"a": "???"})),
"???",
id="inter_dict_to_missing",
),
param(DictConfig(None), None, id="none_dict"),
# comparing to ??? because to_container returns it.
param(DictConfig("???"), "???", id="missing_dict"),
# lists
param(OmegaConf.create([]), [], id="list"),
param(OmegaConf.create([10, "${0}"]), [10, 10], id="list"),
param(OmegaConf.create(["???"]), ["???"], id="list:missing"),
param(OmegaConf.create(["${1}", "???"]), ["???", "???"], id="list:missing"),
param(
ListConfig("${a}", parent=OmegaConf.create({"a": [1, 2]})),
[1, 2],
id="inter_list",
),
param(
ListConfig("${a}", parent=OmegaConf.create({"a": "???"})),
"???",
id="inter_list_to_missing",
),
param(ListConfig(None), None, id="none_list"),
# comparing to ??? because to_container returns it.
param(ListConfig("???"), "???", id="missing_list"),
# Tests for cases where an AnyNode with interpolation is promoted to a DictConfig/ListConfig node
param(
OmegaConf.create({"a": "${z}", "z": {"y": 1}}),
{"a": {"y": 1}, "z": {"y": 1}},
id="any_in_dict_to_dict",
),
param(
OmegaConf.create({"a": "${z}", "z": [1, 2]}),
{"a": [1, 2], "z": [1, 2]},
id="any_in_dict_to_list",
),
param(
OmegaConf.create(["${1}", {"z": {"y": 1}}]),
[{"z": {"y": 1}}, {"z": {"y": 1}}],
id="any_in_list_to_dict",
),
param(
OmegaConf.create(["${1}", [1, 2]]),
[[1, 2], [1, 2]],
id="any_in_list_to_list",
),
],
)
def test_resolve(cfg: Any, expected: Any) -> None:
OmegaConf.resolve(cfg)
# convert output to plain dict to avoid smart OmegaConf eq logic
resolved_plain = OmegaConf.to_container(cfg, resolve=False)
assert resolved_plain == expected


def test_resolve_invalid_input() -> None:
with raises(ValueError):
OmegaConf.resolve("aaa") # type: ignore