Repository: ilevkivskyi/typing_inspect

Issue:
New method is_nonable(t)
This method would return `True` if

 * t is the `NoneType` (`t is type(None)`)
 * t is a (possibly nested) `Union` to `NoneType` (this can be the result of using the `Optional` keyword or of an explicit `Union[type(None), ....]`
 * t is a `TypeVar` with either a `bound` or a `constraints` set, and one of them is nonable


Comments:

Patch:
diff --git a/README.md b/README.md
index 93ad929..4d9924b 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,8 @@ Currently provided functions (see functions docstrings for examples of usage):
   non-generic classes.
 * ``is_union_type(tp)``:
   Test if ``tp`` is a union type.
+* ``is_optional_type(tp)``:
+  Test if ``tp`` is an optional type (either ``type(None)`` or a direct union to it such as in ``Optional[int]``). Nesting and ``TypeVar``s are not unfolded/inspected in this process.
 * ``is_typevar(tp)``:
   Test if ``tp`` represents a type variable.
 * ``is_classvar(tp)``:
diff --git a/typing_inspect.py b/typing_inspect.py
index 6dba71f..ee0de11 100644
--- a/typing_inspect.py
+++ b/typing_inspect.py
@@ -116,6 +116,27 @@ def is_tuple_type(tp):
     return type(tp) is TupleMeta
 
 
+def is_optional_type(tp):
+    """Returns `True` if the type is `type(None)`, or is a direct `Union` to `type(None)`, such as `Optional[T]`.
+
+    NOTE: this method inspects nested `Union` arguments but not `TypeVar` definitions (`bound`/`constraint`). So it
+    will return `False` if
+     - `tp` is a `TypeVar` bound, or constrained to, an optional type
+     - `tp` is a `Union` to a `TypeVar` bound or constrained to an optional type,
+     - `tp` refers to a *nested* `Union` containing an optional type or one of the above.
+
+    Users wishing to check for optionality in types relying on type variables might wish to use this method in
+    combination with `get_constraints` and `get_bound`
+    """
+
+    if tp is type(None):
+        return True
+    elif is_union_type(tp):
+        return any(is_optional_type(tt) for tt in get_args(tp, evaluate=True))
+    else:
+        return False
+
+
 def is_union_type(tp):
     """Test if the type is a union type. Examples::
 
@@ -322,6 +343,36 @@ def get_args(tp, evaluate=None):
     return ()
 
 
+def get_bound(tp):
+    """Returns the type bound to a `TypeVar` if any. It the type is not a `TypeVar`, a `TypeError` is raised
+
+    Examples::
+
+        get_bound(TypeVar('T')) == None
+        get_bound(TypeVar('T', bound=int)) == int
+    """
+
+    if is_typevar(tp):
+        return getattr(tp, '__bound__', None)
+    else:
+        raise TypeError("type is not a `TypeVar`: " + str(tp))
+
+
+def get_constraints(tp):
+    """Returns the constraints of a `TypeVar` if any. It the type is not a `TypeVar`, a `TypeError` is raised
+
+    Examples::
+
+        get_constraints(TypeVar('T')) == ()
+        get_constraints(TypeVar('T', int, str)) == (int, str)
+    """
+
+    if is_typevar(tp):
+        return getattr(tp, '__constraints__', ())
+    else:
+        raise TypeError("type is not a `TypeVar`: " + str(tp))
+
+
 def get_generic_type(obj):
     """Get the generic type of an object if possible, or runtime class otherwise.
     Examples::


Test patch:
diff --git a/test_typing_inspect.py b/test_typing_inspect.py
index f88c617..d499916 100644
--- a/test_typing_inspect.py
+++ b/test_typing_inspect.py
@@ -1,7 +1,8 @@
 from typing_inspect import (
     is_generic_type, is_callable_type, is_tuple_type, is_union_type,
-    is_typevar, is_classvar, get_origin, get_parameters, get_last_args, get_args,
-    get_generic_type, get_generic_bases, get_last_origin, typed_dict_keys,
+    is_optional_type, is_typevar, is_classvar, get_origin, get_parameters,
+    get_last_args, get_args, get_bound, get_constraints, get_generic_type,
+    get_generic_bases, get_last_origin, typed_dict_keys,
 )
 from unittest import TestCase, main, skipIf, skipUnless
 from typing import (
@@ -30,10 +31,11 @@ if PY36:
 
 class IsUtilityTestCase(TestCase):
     def sample_test(self, fun, samples, nonsamples):
+        msg = "Error asserting that %s(%s) is %s"
         for s in samples:
-            self.assertTrue(fun(s))
+            self.assertTrue(fun(s), msg=msg % (fun.__name__, str(s), 'True'))
         for s in nonsamples:
-            self.assertFalse(fun(s))
+            self.assertFalse(fun(s), msg=msg % (fun.__name__, str(s), 'False'))
 
     def test_generic(self):
         T = TypeVar('T')
@@ -68,6 +70,31 @@ class IsUtilityTestCase(TestCase):
         nonsamples = [int, Union[int, int], [], Iterable[Any]]
         self.sample_test(is_union_type, samples, nonsamples)
 
+    def test_optional_type(self):
+        T = TypeVar('T')
+        samples = [type(None),                # none type
+                   Optional[int],             # direct union to none type 1
+                   Optional[T],               # direct union to none type 2
+                   Optional[T][int],          # direct union to none type 3
+                   Union[int, type(None)],    # direct union to none type 4
+                   Union[str, T][type(None)]  # direct union to none type 5
+                   ]
+        # nested unions are supported
+        samples += [Union[str, Optional[int]],      # nested Union 1
+                    Union[T, str][Optional[int]],   # nested Union 2
+                    ]
+        nonsamples = [int, Union[int, int], [], Iterable[Any], T, Union[T, str][int]]
+        # unfortunately current definition sets these ones as non samples too
+        S1 = TypeVar('S1', bound=Optional[int])
+        S2 = TypeVar('S2', type(None), str)
+        S3 = TypeVar('S3', Optional[int], str)
+        S4 = TypeVar('S4', bound=Union[str, Optional[int]])
+        nonsamples += [
+                       S1, S2, S3,                     # typevar bound or constrained to optional
+                       Union[S1, int], S4              # combinations of the above
+                       ]
+        self.sample_test(is_optional_type, samples, nonsamples)
+
     def test_typevar(self):
         T = TypeVar('T')
         S_co = TypeVar('S_co', covariant=True)
@@ -144,6 +171,18 @@ class GetUtilityTestCase(TestCase):
                          (int, Tuple[Optional[int], Optional[int]]))
         self.assertEqual(get_args(Callable[[], T][int], evaluate=True), ([], int,))
 
+    def test_bound(self):
+        T = TypeVar('T')
+        TB = TypeVar('TB', bound=int)
+        self.assertEqual(get_bound(T), None)
+        self.assertEqual(get_bound(TB), int)
+
+    def test_constraints(self):
+        T = TypeVar('T')
+        TC = TypeVar('TC', int, str)
+        self.assertEqual(get_constraints(T), ())
+        self.assertEqual(get_constraints(TC), (int, str))
+
     def test_generic_type(self):
         T = TypeVar('T')
         class Node(Generic[T]): pass