From bcf339f78ff5394d590253025521c7530db88bcf Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Mon, 13 Sep 2021 02:39:11 -0700 Subject: [PATCH] Add is_type utility --- hyperglass/util/tests/__init__.py | 1 + hyperglass/util/tests/test_typing.py | 66 ++++++++++++++++++++++++++++ hyperglass/util/typing.py | 27 ++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 hyperglass/util/tests/__init__.py create mode 100644 hyperglass/util/tests/test_typing.py create mode 100644 hyperglass/util/typing.py diff --git a/hyperglass/util/tests/__init__.py b/hyperglass/util/tests/__init__.py new file mode 100644 index 0000000..9667a75 --- /dev/null +++ b/hyperglass/util/tests/__init__.py @@ -0,0 +1 @@ +"""hyperglass.util tests.""" diff --git a/hyperglass/util/tests/test_typing.py b/hyperglass/util/tests/test_typing.py new file mode 100644 index 0000000..a5ed2c9 --- /dev/null +++ b/hyperglass/util/tests/test_typing.py @@ -0,0 +1,66 @@ +"""Test typing utilities.""" +# flake8: noqa + +# Standard Library +import typing + +# Local +from ..typing import is_type + + +class EmptyTestClass: + pass + + +class EmptySubClass(EmptyTestClass): + pass + + +_string = "Test String" +_string_empty = "" +_dict = {"one": 1, "two": 2} +_dict_empty = dict() +_list = [1, 2, 3] +_list_empty = [] +_set = {"one", "two"} +_set_empty = set() +_tuple = (1, 2, 3) +_tuple_empty = tuple() +_class = EmptyTestClass +_class_instance = EmptyTestClass() +_subclass = EmptySubClass +_subclass_instance = EmptySubClass() + +DictOrString = typing.Union[typing.Dict, str] +ClassOrString = typing.Union[EmptyTestClass, str] + + +def test_is_type(): + checks = ( + ("Non-Empty String is String", True, _string, str), + ("Empty String is String", True, _string_empty, str), + ("Non-Empty Dict is Dict", True, _dict, typing.Dict), + ("Empty Dict is Dict", True, _dict_empty, dict), + ("Non-Empty List is List", True, _list, typing.List), + ("Empty List is List", True, _list_empty, list), + ("Non-Empty Tuple is Tuple", True, _tuple, typing.Tuple), + ("Empty Tuple is Tuple", True, _tuple_empty, tuple), + ("Non-Empty Set is Set", True, _set, typing.Set), + ("Empty Set is Set", True, _set_empty, set), + ("Non-Empty String is Dict or String", True, _string, DictOrString), + ("Non-Empty Dict is Dict or String", True, _dict, DictOrString), + ("Non-Empty List is Dict or String", False, _list, DictOrString), + ("Empty list is Dict or String", False, _list_empty, DictOrString), + ("Class object is Class object", False, _class, _class), + ("Class instance is Class instance", True, _class_instance, _class_instance), + ("Class instance is Class object", True, _class_instance, _class), + ("Subclass instance is Class instance", True, _subclass_instance, _class_instance), + ("Subclass instance is Class object", True, _subclass_instance, _class), + ("Class object is Class or String", False, _subclass, ClassOrString), + ("Class instance is Class or String", True, _class_instance, ClassOrString), + ("Subclass instance is Class or String", True, _subclass_instance, ClassOrString), + ) + for _, expected, value, _type in checks: + result = is_type(value, _type) + if result is not expected: + raise AssertionError(f"Got `{value}`, expected `{str(_type)}`") diff --git a/hyperglass/util/typing.py b/hyperglass/util/typing.py new file mode 100644 index 0000000..760b2fa --- /dev/null +++ b/hyperglass/util/typing.py @@ -0,0 +1,27 @@ +"""Typing utilities.""" + +# Standard Library +import typing +import inspect + + +def is_type(value: typing.Any, *types: typing.Any) -> bool: + """Verify if the type of `value` matches any provided type in `types`. + + Will only check the main type for generics like `Dict` or `List`, but will check the individual + types of generics like `Union` or `Optional`. + + Probably wrong, but seems to work for most cases. + """ + for _type in types: + if _type is None: + return value is None + if inspect.isclass(_type): + return isinstance(value, _type) + origin = typing.get_origin(_type) + if origin is typing.Union: + return any(is_type(value, t) for t in _type.__args__) + if origin is None: + return isinstance(value, type(_type)) + return isinstance(value, origin) + return False