From: Vasek Sraier Date: Mon, 12 Apr 2021 09:27:33 +0000 (+0200) Subject: utils: type parser validator - support for proper Unions with Literals X-Git-Tag: v6.0.0a1~177 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0bedcdd74acd2e6d543dca876ba15ac534cb440d;p=thirdparty%2Fknot-resolver.git utils: type parser validator - support for proper Unions with Literals --- diff --git a/manager/knot_resolver_manager/utils/dataclasses_parservalidator.py b/manager/knot_resolver_manager/utils/dataclasses_parservalidator.py index bb9598bc6..13bee680f 100644 --- a/manager/knot_resolver_manager/utils/dataclasses_parservalidator.py +++ b/manager/knot_resolver_manager/utils/dataclasses_parservalidator.py @@ -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): diff --git a/manager/knot_resolver_manager/utils/types.py b/manager/knot_resolver_manager/utils/types.py index 0ce8f6c86..dc5d7dacc 100644 --- a/manager/knot_resolver_manager/utils/types.py +++ b/manager/knot_resolver_manager/utils/types.py @@ -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") diff --git a/manager/tests/utils/test_types.py b/manager/tests/utils/test_types.py index 2b7aa0234..824d4ea5b 100644 --- a/manager/tests/utils/test_types.py +++ b/manager/tests/utils/test_types.py @@ -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]]