Skip to content

Commit

Permalink
Throws validation error when bare Literal annotation is given for pyt…
Browse files Browse the repository at this point in the history
…hon 3.6 and earlier. Adds explicit dependency on typing_extensions. is_literal_type no longer uses string checking hack.
  • Loading branch information
jordan-schneider committed Mar 26, 2022
1 parent 5d1b801 commit e2d950f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 37 deletions.
17 changes: 7 additions & 10 deletions omegaconf/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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.
Expand Down Expand Up @@ -600,16 +602,11 @@ def is_tuple_annotation(type_: Any) -> bool:

def is_literal_annotation(type_: Any) -> bool:
origin = getattr(type_, "__origin__", None)
if sys.version_info >= (3, 8):
return origin is Literal # pragma: no cover
else:
return (
origin is Literal
or type_ is Literal
or (
"typing_extensions.Literal" in str(type_) and not isinstance(type_, str)
)
) # pragma: no cover
# 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:
Expand Down
14 changes: 11 additions & 3 deletions omegaconf/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,10 +454,18 @@ def __init__(
f"LiteralNode can only operate on Literal annotation ({literal_type})"
)
self.literal_type = literal_type
if hasattr(self.literal_type, "__args__"):
self.fields = list(self.literal_type.__args__) # pragma: no cover
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
self.fields = list(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,
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_extensionsl;python_version<='3.7'
50 changes: 38 additions & 12 deletions tests/structured_conf/data/attr_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,21 +186,47 @@ class EnumConfig:
interpolation: Color = II("with_default")


@attr.s(auto_attribs=True)
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"
if sys.version_info >= (3, 7): # pragma: no cover

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

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

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)
# 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)
Expand Down
50 changes: 38 additions & 12 deletions tests/structured_conf/data/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,21 +187,47 @@ class EnumConfig:
interpolation: Color = II("with_default")


@dataclass
class LiteralConfig:
# with default value
with_default: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = "foo"
if sys.version_info >= (3, 7): # pragma: no cover

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

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

# interpolation, will inherit the type and value of `with_default'
interpolation: Literal["foo", "bar", True, b"baz", 5, Color.GREEN] = II(
"with_default"
)
# 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
Expand Down
4 changes: 4 additions & 0 deletions tests/structured_conf/test_structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,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

0 comments on commit e2d950f

Please sign in to comment.