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

Adds literal node #865

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3d3ae1d
Adds literal node
jordan-schneider Feb 26, 2022
9338386
Missed a not
jordan-schneider Feb 26, 2022
b78d4dc
Adds additional LiteralNode tests. Imports Literal based on python ve…
jordan-schneider Feb 26, 2022
6d302ab
Adds no cover pragma to typing import branch
jordan-schneider Feb 26, 2022
3c115d7
Adds additional fixes for earlier python versions.
jordan-schneider Feb 26, 2022
1592df4
Used the wrong type
jordan-schneider Feb 26, 2022
3b8a82f
Update omegaconf/_utils.py
Mar 13, 2022
80f9861
Update omegaconf/_utils.py
Mar 13, 2022
0737ec5
Throws validation error when bare Literal annotation is given for pyt…
jordan-schneider Mar 14, 2022
eb4b20c
Merge remote-tracking branch 'upstream/master'
jordan-schneider Mar 14, 2022
a0e2b35
Fixes typo in typing_extensions requirement.
jordan-schneider Mar 14, 2022
f3b3f75
Adds literal node
jordan-schneider Feb 26, 2022
694a032
Missed a not
jordan-schneider Feb 26, 2022
1bd9c93
Adds additional LiteralNode tests. Imports Literal based on python ve…
jordan-schneider Mar 26, 2022
fb32fa5
Adds no cover pragma to typing import branch
jordan-schneider Feb 26, 2022
89414a8
Adds additional fixes for earlier python versions.
jordan-schneider Feb 26, 2022
c86cec5
Used the wrong type
jordan-schneider Feb 26, 2022
59eb792
Update omegaconf/_utils.py
Mar 13, 2022
5d1b801
Update omegaconf/_utils.py
Mar 13, 2022
e2d950f
Throws validation error when bare Literal annotation is given for pyt…
jordan-schneider Mar 14, 2022
4fa9593
Fixes typo in typing_extensions requirement.
jordan-schneider Mar 14, 2022
d321a4a
Merge branch 'master' of github.com:jordan-schneider/omegaconf
jordan-schneider Mar 26, 2022
b69fd38
Formats according to new black version
jordan-schneider Mar 26, 2022
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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 20.8b1
rev: 22.1.0
hooks:
- id: black
language_version: python3.8
Expand All @@ -21,4 +21,4 @@ repos:
hooks:
- id: mypy
args: [--strict]
additional_dependencies: ['pytest']
additional_dependencies: ['pytest', 'types-dataclasses']
2 changes: 2 additions & 0 deletions omegaconf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
EnumNode,
FloatNode,
IntegerNode,
LiteralNode,
StringNode,
ValueNode,
)
Expand Down Expand Up @@ -55,6 +56,7 @@
"BytesNode",
"BooleanNode",
"EnumNode",
"LiteralNode",
"FloatNode",
"MISSING",
"SI",
Expand Down
26 changes: 25 additions & 1 deletion omegaconf/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
except ImportError: # pragma: no cover
attr = None # type: ignore # pragma: no cover

if sys.version_info >= (3, 8):
from typing import Literal # pragma: no cover
else:
from typing_extensions import Literal # pragma: no cover
if sys.version_info < (3, 7):
from typing_extensions import _Literal # type: ignore # pragma: no cover


# Regexprs to match key paths like: a.b, a[b], ..a[c].d, etc.
# We begin by matching the head (in these examples: a, a, ..a).
Expand Down Expand Up @@ -593,6 +600,15 @@ def is_tuple_annotation(type_: Any) -> bool:
return origin is tuple # pragma: no cover


def is_literal_annotation(type_: Any) -> bool:
origin = getattr(type_, "__origin__", None)
# For python 3.6 and earllier typing_extensions.Literal does not have an origin attribute, and
# Literal is an instance of an internal _Literal class that we can check against.
if sys.version_info < (3, 7):
return type(type_) is _Literal # pragma: no cover
return origin is Literal # pragma: no cover


def is_dict_subclass(type_: Any) -> bool:
return type_ is not None and isinstance(type_, type) and issubclass(type_, Dict)

Expand Down Expand Up @@ -829,13 +845,21 @@ def type_str(t: Any, include_module_name: bool = False) -> str:
return "Any"
if t is ...:
return "..."
if (
isinstance(t, int)
or isinstance(t, str)
or isinstance(t, bytes)
or isinstance(t, Enum)
):
# only occurs when using typing.Literal after 3.8
return str(t) # pragma: no cover

if sys.version_info < (3, 7, 0): # pragma: no cover
# Python 3.6
if hasattr(t, "__name__"):
name = str(t.__name__)
else:
if t.__origin__ is not None:
if getattr(t, "__origin__", None) is not None:
name = type_str(t.__origin__)
else:
name = str(t)
Expand Down
65 changes: 65 additions & 0 deletions omegaconf/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
_is_interpolation,
get_type_of,
get_value_kind,
is_literal_annotation,
is_primitive_container,
type_str,
)
Expand Down Expand Up @@ -438,6 +439,70 @@ def __deepcopy__(self, memo: Dict[int, Any]) -> "EnumNode":
return res


class LiteralNode(ValueNode):
def __init__(
self,
literal_type: Any, # cannot Type[Literal] because Literal requires an argument
value: Optional[Union[Enum, str, int, bool]] = None,
key: Any = None,
parent: Optional[Container] = None,
is_optional: bool = True,
flags: Optional[Dict[str, bool]] = None,
):
if not is_literal_annotation(literal_type):
raise ValidationError(
f"LiteralNode can only operate on Literal annotation ({literal_type})"
)
self.literal_type = literal_type
if hasattr(self.literal_type, "__args__"): # pragma: no cover
# python 3.7 and above
args = self.literal_type.__args__
self.fields = list(args) if args is not None else []
elif hasattr(self.literal_type, "__values__"): # pragma: no cover
# python 3.6 and below
values = self.literal_type.__values__
self.fields = list(values) if values is not None else []
else: # pragma: no cover
raise ValidationError(
f"literal_type={literal_type} is a literal but has no __args__ or __values__"
)
super().__init__(
parent=parent,
value=value,
metadata=Metadata(
key=key,
optional=is_optional,
ref_type=literal_type,
object_type=literal_type,
flags=flags,
),
)

def _validate_and_convert_impl(self, value: Any) -> Any:
return self.validate_and_convert_to_literal(
enum_type=self.literal_type, value=value
)

def validate_and_convert_to_literal(self, enum_type: Type[Enum], value: Any) -> Any:
if value not in self.fields:
raise ValidationError(
f"Value $VALUE ($VALUE_TYPE) is not a valid input for {enum_type}"
)
index = self.fields.index(value)
if not isinstance(value, type(self.fields[index])):
raise ValidationError(
f"Value $VALUE ($VALUE_TYPE) is not a valid input for {enum_type} because "
f"type(value)={type(value)} but the matching literal value's type={type(self.fields[index])}"
)

return value

def __deepcopy__(self, memo: Dict[int, Any]) -> "LiteralNode":
res = LiteralNode(literal_type=self.literal_type)
self._deepcopy_impl(res, memo)
return res


class InterpolationResultNode(ValueNode):
"""
Special node type, used to wrap interpolation results.
Expand Down
10 changes: 10 additions & 0 deletions omegaconf/omegaconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
is_dict_annotation,
is_int,
is_list_annotation,
is_literal_annotation,
is_primitive_container,
is_primitive_dict,
is_primitive_list,
Expand All @@ -67,6 +68,7 @@
EnumNode,
FloatNode,
IntegerNode,
LiteralNode,
StringNode,
ValueNode,
)
Expand Down Expand Up @@ -1047,6 +1049,14 @@ def _node_wrap(
)
elif type_ == Any or type_ is None:
node = AnyNode(value=value, key=key, parent=parent)
elif is_literal_annotation(type_):
node = LiteralNode(
literal_type=type_,
value=value,
key=key,
parent=parent,
is_optional=is_optional,
)
elif issubclass(type_, Enum):
node = EnumNode(
enum_type=type_,
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ antlr4-python3-runtime==4.8
PyYAML>=5.1.0
# Use dataclasses backport for Python 3.6.
dataclasses;python_version=='3.6'
typing_extensions;python_version<='3.7'
46 changes: 45 additions & 1 deletion tests/structured_conf/data/attr_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from tests import Color

if sys.version_info >= (3, 8): # pragma: no cover
from typing import TypedDict
from typing import Literal, TypedDict
else:
from typing_extensions import Literal

# attr is a dependency of pytest which means it's always available when testing with pytest.
importorskip("attr")
Expand Down Expand Up @@ -184,6 +186,48 @@ class EnumConfig:
interpolation: Color = II("with_default")


if sys.version_info >= (3, 7): # pragma: no cover

@attr.s(auto_attribs=True)
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"

# default is None
null_default: Optional[
Literal["foo", "bar", True, b"baz", 5, Color.GREEN]
] = None

# explicit no default
mandatory_missing: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = MISSING

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)

else: # pragma: no cover
# bare literals throw errors for python 3.7+. They're against spec for python 3.6 and earlier,
# but we should test that they fail to validate anyway.
@attr.s(auto_attribs=True)
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"

# default is None
null_default: Optional[
Literal["foo", "bar", True, b"baz", 5, Color.GREEN]
] = None
# explicit no default
mandatory_missing: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = MISSING

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)
no_args: Optional[Literal] = None # type: ignore


@attr.s(auto_attribs=True)
class ConfigWithList:
list1: List[int] = [1, 2, 3]
Expand Down
46 changes: 45 additions & 1 deletion tests/structured_conf/data/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from tests import Color

if sys.version_info >= (3, 8): # pragma: no cover
from typing import TypedDict
from typing import Literal, TypedDict
else:
from typing_extensions import Literal

# skip test if dataclasses are not available
importorskip("dataclasses")
Expand Down Expand Up @@ -185,6 +187,48 @@ class EnumConfig:
interpolation: Color = II("with_default")


if sys.version_info >= (3, 7): # pragma: no cover

@dataclass
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"

# default is None
null_default: Optional[
Literal["foo", "bar", True, b"baz", 5, Color.GREEN]
] = None

# explicit no default
mandatory_missing: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = MISSING

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)

else: # pragma: no cover
# bare literals throw errors for python 3.7+. They're against spec for python 3.6 and earlier,
# but we should test that they fail to validate anyway.
@dataclass
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"

# default is None
null_default: Optional[
Literal["foo", "bar", True, b"baz", 5, Color.GREEN]
] = None
# explicit no default
mandatory_missing: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = MISSING

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)
no_args: Optional[Literal] = None # type: ignore


@dataclass
class ConfigWithList:
list1: List[int] = field(default_factory=lambda: [1, 2, 3])
Expand Down
11 changes: 11 additions & 0 deletions tests/structured_conf/test_structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ class EnumConfigAssignments:
illegal = ["foo", True, b"RED", False, 4, 1.0]


class LiteralConfigAssignments:
legal = ["foo", "bar", True, b"baz", 5, Color.GREEN]
illegal = ["fuh", False, 4, 1.0, b"feh", Color.RED]


class IntegersConfigAssignments:
legal = [("10", 10), ("-10", -10), 100, 0, 1]
illegal = ["foo", 1.0, float("inf"), b"123", float("nan"), Color.BLUE, True]
Expand Down Expand Up @@ -265,13 +270,15 @@ def validate(cfg: DictConfig) -> None:
("BytesConfig", BytesConfigAssignments, {}),
("StringConfig", StringConfigAssignments, {}),
("EnumConfig", EnumConfigAssignments, {}),
("LiteralConfig", LiteralConfigAssignments, {}),
# Use instance to build config
("BoolConfig", BoolConfigAssignments, {"with_default": False}),
("IntegersConfig", IntegersConfigAssignments, {"with_default": 42}),
("FloatConfig", FloatConfigAssignments, {"with_default": 42.0}),
("BytesConfig", BytesConfigAssignments, {"with_default": b"bin"}),
("StringConfig", StringConfigAssignments, {"with_default": "fooooooo"}),
("EnumConfig", EnumConfigAssignments, {"with_default": Color.BLUE}),
("LiteralConfig", LiteralConfigAssignments, {"with_default": "foo"}),
("AnyTypeConfig", AnyTypeConfigAssignments, {}),
],
)
Expand Down Expand Up @@ -310,6 +317,10 @@ def validate(input_: Any, expected: Any) -> None:
with raises(ValidationError):
conf.mandatory_missing = illegal_value

if hasattr(conf, "no_args"):
with raises(ValidationError):
conf.no_args = illegal_value

# Test assignment of legal values
for legal_value in assignment_data.legal:
expected_data = legal_value
Expand Down
Loading