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

541 oc.select #687

Merged
merged 1 commit into from
Apr 18, 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
59 changes: 59 additions & 0 deletions docs/source/custom_resolvers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,65 @@ This resolver can be useful for instance to parse environment variables:
type: int, value: 3308


.. _oc.select:

oc.select
^^^^^^^^^
odelalleau marked this conversation as resolved.
Show resolved Hide resolved
``oc.select`` enables selection similar to that performed with node interpolation, but is a bit more flexible.
Using ``oc.select``, you can provide a default value to use in case the primary interpolation key is not found.
The following example uses "/tmp" as the default value for the node output:

.. doctest::

>>> cfg = OmegaConf.create({
... "a": "Saving output to ${oc.select:output,/tmp}"
... })
>>> print(cfg.a)
Saving output to /tmp
>>> cfg.output = "/etc/config"
>>> print(cfg.a)
Saving output to /etc/config

``oc.select`` can also be used to select keys that are otherwise illegal interpolation keys.
The following example has a key with a colon. Such a key looks like a custom resolver and therefore
cannot be accessed using a regular interpolation:

.. doctest::

>>> cfg = OmegaConf.create({
... # yes, there is a : in this key
... "a:b": 10,
... "bad": "${a:b}",
... "good": "${oc.select:'a:b'}",
... })
>>> print(cfg.bad)
Traceback (most recent call last):
...
UnsupportedInterpolationType: Unsupported interpolation type a
>>> print(cfg.good)
10

Another scenario where ``oc.select`` can be useful is if you want to select a missing value.

.. doctest::

>>> cfg = OmegaConf.create({
... "missing": "???",
... "interpolation": "${missing}",
... "select": "${oc.select:missing}",
... "with_default": "${oc.select:missing,default value}",
... }
... )
...
>>> print(cfg.interpolation)
Traceback (most recent call last):
...
InterpolationToMissingValueError: MissingMandatoryValue while ...
>>> print(cfg.select)
None
>>> print(cfg.with_default)
default value

.. _oc.dict.{keys,values}:

oc.dict.{keys,value}
Expand Down
1 change: 1 addition & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ OmegaConf comes with a set of built-in custom resolvers:
* :ref:`oc.decode`: Parsing an input string using interpolation grammar
* :ref:`oc.deprecated`: Deprecate a key in your config
* :ref:`oc.env`: Accessing environment variables
* :ref:`oc.select`: Selecting an interpolation key, similar to interpolation but more flexible
* :ref:`oc.dict.{keys,values}`: Viewing the keys or the values of a dictionary as a list


Expand Down
1 change: 1 addition & 0 deletions news/541.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add oc.select, enabling node selection with a default value to use if the node cannot be selected
1 change: 1 addition & 0 deletions omegaconf/omegaconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def register_default_resolvers() -> None:
OmegaConf.register_new_resolver("oc.decode", oc.decode)
OmegaConf.register_new_resolver("oc.deprecated", oc.deprecated)
OmegaConf.register_new_resolver("oc.env", oc.env)
OmegaConf.register_new_resolver("oc.select", oc.select)
OmegaConf.register_new_resolver("oc.dict.keys", oc.dict.keys)
OmegaConf.register_new_resolver("oc.dict.values", oc.dict.values)
OmegaConf.legacy_register_resolver("env", env)
Expand Down
12 changes: 12 additions & 0 deletions omegaconf/resolvers/oc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,22 @@ def deprecated(
return target_node


def select(
odelalleau marked this conversation as resolved.
Show resolved Hide resolved
key: str,
default: Any = _DEFAULT_MARKER_,
*,
_parent_: Container,
) -> Any:
from omegaconf._impl import select_value

return select_value(cfg=_parent_, key=key, absolute_key=True, default=default)


__all__ = [
"create",
"decode",
"deprecated",
"dict",
"env",
"select",
]
55 changes: 55 additions & 0 deletions tests/interpolation/built_in_resolvers/test_legacy_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import re
from typing import Any

from pytest import mark, warns

from omegaconf import OmegaConf


def test_legacy_env_is_cached(monkeypatch: Any) -> None:
monkeypatch.setenv("FOOBAR", "1234")
c = OmegaConf.create({"foobar": "${env:FOOBAR}"})
with warns(UserWarning):
before = c.foobar
monkeypatch.setenv("FOOBAR", "3456")
assert c.foobar == before


@mark.parametrize(
"value,expected",
[
# bool
("false", False),
("true", True),
# int
("10", 10),
("-10", -10),
# float
("10.0", 10.0),
("-10.0", -10.0),
# strings
("off", "off"),
("no", "no"),
("on", "on"),
("yes", "yes"),
(">1234", ">1234"),
(":1234", ":1234"),
("/1234", "/1234"),
# yaml strings are not getting parsed by the env resolver
("foo: bar", "foo: bar"),
("foo: \n - bar\n - baz", "foo: \n - bar\n - baz"),
# more advanced uses of the grammar
("ab \\{foo} cd", "ab \\{foo} cd"),
("ab \\\\{foo} cd", "ab \\\\{foo} cd"),
(" 1 2 3 ", " 1 2 3 "),
("\t[1, 2, 3]\t", "\t[1, 2, 3]\t"),
(" {a: b}\t ", " {a: b}\t "),
],
)
def test_legacy_env_values_are_typed(
monkeypatch: Any, value: Any, expected: Any
) -> None:
monkeypatch.setenv("MYKEY", value)
c = OmegaConf.create({"my_key": "${env:MYKEY}"})
with warns(UserWarning, match=re.escape("The `env` resolver is deprecated")):
assert c.my_key == expected
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from typing import Any, Optional

from pytest import mark, param, raises, warns
from pytest import mark, param, raises

from omegaconf import OmegaConf
from omegaconf._utils import _ensure_container
Expand Down Expand Up @@ -89,15 +89,6 @@ def test_env_interpolation_error(
OmegaConf.select(cfg, key)


def test_legacy_env_is_cached(monkeypatch: Any) -> None:
monkeypatch.setenv("FOOBAR", "1234")
c = OmegaConf.create({"foobar": "${env:FOOBAR}"})
with warns(UserWarning):
before = c.foobar
monkeypatch.setenv("FOOBAR", "3456")
assert c.foobar == before


def test_env_is_not_cached(monkeypatch: Any) -> None:
monkeypatch.setenv("FOOBAR", "1234")
c = OmegaConf.create({"foobar": "${oc.env:FOOBAR}"})
Expand All @@ -116,46 +107,6 @@ def test_env_preserves_string(monkeypatch: Any, value: str) -> None:
assert c.my_key == value


@mark.parametrize(
"value,expected",
[
# bool
("false", False),
("true", True),
# int
("10", 10),
("-10", -10),
# float
("10.0", 10.0),
("-10.0", -10.0),
# strings
("off", "off"),
("no", "no"),
("on", "on"),
("yes", "yes"),
(">1234", ">1234"),
(":1234", ":1234"),
("/1234", "/1234"),
# yaml strings are not getting parsed by the env resolver
("foo: bar", "foo: bar"),
("foo: \n - bar\n - baz", "foo: \n - bar\n - baz"),
# more advanced uses of the grammar
("ab \\{foo} cd", "ab \\{foo} cd"),
("ab \\\\{foo} cd", "ab \\\\{foo} cd"),
(" 1 2 3 ", " 1 2 3 "),
("\t[1, 2, 3]\t", "\t[1, 2, 3]\t"),
(" {a: b}\t ", " {a: b}\t "),
],
)
def test_legacy_env_values_are_typed(
monkeypatch: Any, value: Any, expected: Any
) -> None:
monkeypatch.setenv("MYKEY", value)
c = OmegaConf.create({"my_key": "${env:MYKEY}"})
with warns(UserWarning, match=re.escape("The `env` resolver is deprecated")):
assert c.my_key == expected


def test_env_default_none(monkeypatch: Any) -> None:
monkeypatch.delenv("MYKEY", raising=False)
c = OmegaConf.create({"my_key": "${oc.env:MYKEY, null}"})
Expand Down
124 changes: 124 additions & 0 deletions tests/interpolation/built_in_resolvers/test_oc_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from typing import Any

from pytest import mark

from omegaconf import OmegaConf
odelalleau marked this conversation as resolved.
Show resolved Hide resolved


def test_oc_select_abs() -> None:
cfg = OmegaConf.create(
{
"a0": "${k}",
"a1": "${oc.select:k}",
"a2": "${oc.select:k, zzz}",
"k": 10,
},
)
assert cfg.a0 == cfg.a1 == cfg.a2 == 10


def test_oc_select_missing() -> None:
cfg = OmegaConf.create(
{
"a": "${oc.select:missing}",
"b": "${oc.select:missing, default value}",
"missing": "???",
},
)
assert cfg.a is None
assert cfg.b == "default value"


def test_oc_select_none() -> None:
cfg = OmegaConf.create(
{
"a": "${oc.select:none}",
"b": "${oc.select:none, default value}",
"none": None,
},
)
assert cfg.a is None
assert cfg.b is None


def test_oc_select_relative() -> None:
cfg = OmegaConf.create(
{
"a0": "${.k}",
"a1": "${oc.select:.k}",
"a2": "${oc.select:.k, zzz}",
"k": 10,
},
)
assert cfg.a0 == cfg.a1 == cfg.a2 == 10


def test_oc_nested_select_abs() -> None:
cfg = OmegaConf.create(
{
"nested": {
"a0": "${k}",
"a1": "${oc.select:k}",
"a2": "${oc.select:k,zzz}",
},
"k": 10,
},
)

n = cfg.nested
assert n.a0 == n.a1 == n.a2 == 10


def test_oc_nested_select_relative_same_level() -> None:
cfg = OmegaConf.create(
{
"nested": {
"a0": "${.k}",
"a1": "${oc.select:.k}",
"a2": "${oc.select:.k, zzz}",
"k": 20,
},
},
)

n = cfg.nested
assert n.a0 == n.a1 == n.a2 == 20


def test_oc_nested_select_relative_level_up() -> None:
cfg = OmegaConf.create(
{
"nested": {
"a0": "${..k}",
"a1": "${oc.select:..k}",
"a2": "${oc.select:..k, zzz}",
"k": 20,
},
"k": 10,
},
)

n = cfg.nested
assert n.a0 == n.a1 == n.a2 == 10


@mark.parametrize(
("key", "expected"),
[
("a0", 10),
("a1", 11),
("a2", None),
("a3", 20),
],
)
def test_oc_select_using_default(key: str, expected: Any) -> None:
cfg = OmegaConf.create(
{
"a0": "${oc.select:zz, 10}",
"a1": "${oc.select:.zz, 11}",
"a2": "${oc.select:zz, null}",
"a3": "${oc.select:zz, ${value}}",
"value": 20,
},
)
assert cfg[key] == expected