From e2d950fc37f5f65aba64a2c831a6cb855d325566 Mon Sep 17 00:00:00 2001 From: Jordan Schneider Date: Mon, 14 Mar 2022 10:36:28 -0500 Subject: [PATCH] Throws validation error when bare Literal annotation is given for python 3.6 and earlier. Adds explicit dependency on typing_extensions. is_literal_type no longer uses string checking hack. --- omegaconf/_utils.py | 17 +++---- omegaconf/nodes.py | 14 ++++-- requirements/base.txt | 1 + tests/structured_conf/data/attr_classes.py | 50 ++++++++++++++----- tests/structured_conf/data/dataclasses.py | 50 ++++++++++++++----- .../structured_conf/test_structured_config.py | 4 ++ 6 files changed, 99 insertions(+), 37 deletions(-) diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py index daa4f50d0..e7956f80a 100644 --- a/omegaconf/_utils.py +++ b/omegaconf/_utils.py @@ -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. @@ -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: diff --git a/omegaconf/nodes.py b/omegaconf/nodes.py index 26c508dd0..a904f4919 100644 --- a/omegaconf/nodes.py +++ b/omegaconf/nodes.py @@ -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, diff --git a/requirements/base.txt b/requirements/base.txt index dab8a132e..1a35a1691 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -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' \ No newline at end of file diff --git a/tests/structured_conf/data/attr_classes.py b/tests/structured_conf/data/attr_classes.py index deaab1363..60de5642e 100644 --- a/tests/structured_conf/data/attr_classes.py +++ b/tests/structured_conf/data/attr_classes.py @@ -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) diff --git a/tests/structured_conf/data/dataclasses.py b/tests/structured_conf/data/dataclasses.py index 7ff624a06..853167fe1 100644 --- a/tests/structured_conf/data/dataclasses.py +++ b/tests/structured_conf/data/dataclasses.py @@ -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 diff --git a/tests/structured_conf/test_structured_config.py b/tests/structured_conf/test_structured_config.py index d624d19b5..81a345fb4 100644 --- a/tests/structured_conf/test_structured_config.py +++ b/tests/structured_conf/test_structured_config.py @@ -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