]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
utils: type parser validator - support for proper Unions with Literals
authorVasek Sraier <git@vakabus.cz>
Mon, 12 Apr 2021 09:27:33 +0000 (11:27 +0200)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:52 +0000 (16:17 +0200)
manager/knot_resolver_manager/utils/dataclasses_parservalidator.py
manager/knot_resolver_manager/utils/types.py
manager/tests/utils/test_types.py

index bb9598bc61c3e222e3db57d207048f8a23990e05..13bee680f1aa34c48962b5c57c7b1d7603a2e322 100644 (file)
@@ -6,11 +6,12 @@ import yaml
 from knot_resolver_manager.utils.types import (
     get_generic_type_argument,
     get_generic_type_arguments,
-    get_optional_inner_type,
     is_dict,
     is_list,
-    is_optional,
+    is_literal,
+    is_none_type,
     is_tuple,
+    is_union,
 )
 
 from ..compat.dataclasses import is_dataclass
@@ -25,24 +26,52 @@ def _from_dictlike_obj(cls: Any, obj: Any, default: Any, use_default: bool) -> A
     if obj is None and use_default:
         return default
 
-    # primitive types
-    if cls in (int, float, str):
-        return cls(obj)
-
-    # Optional[T]
-    if is_optional(cls):
+    # NoneType
+    elif is_none_type(cls):
         if obj is None:
             return None
         else:
-            return _from_dictlike_obj(get_optional_inner_type(cls), obj, ..., False)
+            raise ValidationException(f"Expected None, found {obj}")
+    
+    # Union[*variants] (handles Optional[T] due to the way the typing system works)
+    elif is_union(cls):
+        variants = get_generic_type_arguments(cls)
+        for v in variants:
+            try:
+                return _from_dictlike_obj(v, obj, ..., False)
+            except ValidationException:
+                pass
+        raise ValidationException("Union {cls} could not be parsed - parsing of all variants failed")
+
+    # after this, there is no place for a None object
+    elif obj is None:
+        raise ValidationException(f"Unexpected None value for type {cls}")
+
+    # primitive types
+    if cls in (int, float, str):
+        try:
+            return cls(obj)
+        except ValueError as e:
+            raise ValidationException("Failed to parse primitive type {cls}, value {obj}", e)
+
+    # Literal[T]
+    elif is_literal(cls):
+        expected = get_generic_type_argument(cls)
+        if obj == expected:
+            return obj
+        else:
+            raise ValidationException("Literal {cls} is not matched with the value {obj}")
 
     # Dict[K,V]
     elif is_dict(cls):
         key_type, val_type = get_generic_type_arguments(cls)
-        return {
-            _from_dictlike_obj(key_type, key, ..., False): _from_dictlike_obj(val_type, val, ..., False)
-            for key, val in obj.items()
-        }
+        try:
+            return {
+                _from_dictlike_obj(key_type, key, ..., False): _from_dictlike_obj(val_type, val, ..., False)
+                for key, val in obj.items()
+            }
+        except AttributeError as e:
+            raise ValidationException(f"Expected dict-like object, but failed to access its .items() method. Value was {obj}")
 
     # List[T]
     elif is_list(cls):
index 0ce8f6c867f1c80b4a8c0222f3d558fab1aac636..dc5d7dacc26ec856412504b37180089dfe8f2b3a 100644 (file)
@@ -1,5 +1,7 @@
 from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
 
+from typing_extensions import Literal
+
 NoneType = type(None)
 
 
@@ -23,8 +25,12 @@ def is_tuple(tp: Any) -> bool:
 
 
 def is_union(tp: Any) -> bool:
-    """Returns False if it is Union but looks like Optional"""
-    return not is_optional(tp) and getattr(tp, "__origin__", None) == Union
+    """ Returns true even for optional types, because they are just a Union[T, NoneType] """
+    return getattr(tp, "__origin__", None) == Union
+
+
+def is_literal(tp: Any) -> bool:
+    return getattr(tp, "__origin__", None) == Literal
 
 
 def get_generic_type_arguments(tp: Any) -> List[Any]:
@@ -39,6 +45,18 @@ def get_generic_type_argument(tp: Any) -> Any:
     return args[0]
 
 
+def is_none_type(tp: Any) -> bool:
+    return tp is None or tp == NoneType
+
+
+class _LiteralEnum:
+    def __getitem__(self, args: Tuple[Union[str,int,bytes], ...]) -> Any:
+        lits = tuple(Literal[x] for x in args)
+        return Union[lits]  # pyright: reportGeneralTypeIssues=false
+
+LiteralEnum = _LiteralEnum()
+
+
 T = TypeVar("T")
 
 
index 2b7aa02342aca3cf3283dcb52d4facc1983ae33d..824d4ea5be5290232d09ee2bec98ec5af46cf78c 100644 (file)
@@ -1,8 +1,15 @@
-from typing import List
+from typing import List, Union
 
-from knot_resolver_manager.utils.types import is_list
+from typing_extensions import Literal
+
+from knot_resolver_manager.utils.types import LiteralEnum, is_list
 
 
 def test_is_list():
     assert is_list(List[str])
     assert is_list(List[int])
+
+
+def test_literal_enum():
+    assert LiteralEnum[5, "test"] == Union[Literal[5], Literal["test"]]
+    assert LiteralEnum["str", 5] == Union[Literal["str"], Literal[5]]