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
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):
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
+from typing_extensions import Literal
+
NoneType = type(None)
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]:
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")
-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]]