Repository: python/mypy

Issue:
Recognize `callable` in conditional type binding
If I run mypy against the following code:

``` python
from typing import Callable, Union

SomeFieldType = Union[str, Callable[[str], int]]

class SomeClass(object):
    def __init__(self, some_field: SomeFieldType) -> None:
        self.some_field = some_field

    def do_something(self) -> None:
        if callable(self.some_field):
            self.some_field('foobar')
```

it complains about self.some_field not being callable:

```
test_mypy.py: note: In member "do_something" of class "SomeClass":
test_mypy.py:11: error: "str" not callable
```

Perhaps mypy could be made aware of the "callable(...)" statements? Thanks!



Discussion:
I see. Normally unions can be disambiguated using isinstance(), but that doesn't really work for Callable.

If this is desirable, probably adding some more code to the `find_isinstance_check` analysis might resolve this


Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2506,6 +2506,76 @@ def conditional_type_map(expr: Expression,
         return {}, {}
 
 
+def partition_by_callable(type: Optional[Type]) -> Tuple[List[Type], List[Type]]:
+    """Takes in a type and partitions that type into callable subtypes and
+    uncallable subtypes.
+
+    Thus, given:
+    `callables, uncallables = partition_by_callable(type)`
+
+    If we assert `callable(type)` then `type` has type Union[*callables], and
+    If we assert `not callable(type)` then `type` has type Union[*uncallables]
+
+    Guaranteed to not return [], []"""
+    if isinstance(type, FunctionLike) or isinstance(type, TypeType):
+        return [type], []
+
+    if isinstance(type, AnyType):
+        return [type], [type]
+
+    if isinstance(type, UnionType):
+        callables = []
+        uncallables = []
+        for subtype in type.items:
+            subcallables, subuncallables = partition_by_callable(subtype)
+            callables.extend(subcallables)
+            uncallables.extend(subuncallables)
+        return callables, uncallables
+
+    if isinstance(type, TypeVarType):
+        return partition_by_callable(type.erase_to_union_or_bound())
+
+    if isinstance(type, Instance):
+        method = type.type.get_method('__call__')
+        if method:
+            callables, uncallables = partition_by_callable(method.type)
+            if len(callables) and not len(uncallables):
+                # Only consider the type callable if its __call__ method is
+                # definitely callable.
+                return [type], []
+        return [], [type]
+
+    return [], [type]
+
+
+def conditional_callable_type_map(expr: Expression,
+                                  current_type: Optional[Type],
+                                  ) -> Tuple[TypeMap, TypeMap]:
+    """Takes in an expression and the current type of the expression.
+
+    Returns a 2-tuple: The first element is a map from the expression to
+    the restricted type if it were callable. The second element is a
+    map from the expression to the type it would hold if it weren't
+    callable."""
+    if not current_type:
+        return {}, {}
+
+    if isinstance(current_type, AnyType):
+        return {}, {}
+
+    callables, uncallables = partition_by_callable(current_type)
+
+    if len(callables) and len(uncallables):
+        callable_map = {expr: UnionType.make_union(callables)} if len(callables) else None
+        uncallable_map = {expr: UnionType.make_union(uncallables)} if len(uncallables) else None
+        return callable_map, uncallable_map
+
+    elif len(callables):
+        return {}, None
+
+    return None, {}
+
+
 def is_true_literal(n: Expression) -> bool:
     return (refers_to_fullname(n, 'builtins.True')
             or isinstance(n, IntExpr) and n.value == 1)
@@ -2579,7 +2649,7 @@ def find_isinstance_check(node: Expression,
                           type_map: Dict[Expression, Type],
                           ) -> Tuple[TypeMap, TypeMap]:
     """Find any isinstance checks (within a chain of ands).  Includes
-    implicit and explicit checks for None.
+    implicit and explicit checks for None and calls to callable.
 
     Return value is a map of variables to their types if the condition
     is true and a map of variables to their types if the condition is false.
@@ -2600,6 +2670,11 @@ def find_isinstance_check(node: Expression,
                 vartype = type_map[expr]
                 type = get_isinstance_type(node.args[1], type_map)
                 return conditional_type_map(expr, vartype, type)
+        elif refers_to_fullname(node.callee, 'builtins.callable'):
+            expr = node.args[0]
+            if expr.literal == LITERAL_TYPE:
+                vartype = type_map[expr]
+                return conditional_callable_type_map(expr, vartype)
     elif (isinstance(node, ComparisonExpr) and experiments.STRICT_OPTIONAL):
         # Check for `x is None` and `x is not None`.
         is_not = node.operators == ['is not']


Test patch:
diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py
--- a/mypy/test/testcheck.py
+++ b/mypy/test/testcheck.py
@@ -28,6 +28,7 @@
 # List of files that contain test case descriptions.
 files = [
     'check-basic.test',
+    'check-callable.test',
     'check-classes.test',
     'check-expressions.test',
     'check-statements.test',
diff --git a/test-data/unit/check-callable.test b/test-data/unit/check-callable.test
new file mode 100644
--- /dev/null
+++ b/test-data/unit/check-callable.test
@@ -0,0 +1,345 @@
+[case testCallableDef]
+def f() -> None: pass
+
+if callable(f):
+    f()
+else:
+    f += 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableLambda]
+f = lambda: None
+
+if callable(f):
+    f()
+else:
+    f += 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableNotCallable]
+x = 5
+
+if callable(x):
+    x()
+else:
+    x += 5
+
+[builtins fixtures/callable.pyi]
+
+[case testUnion]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, Callable[[], str]]
+
+if callable(x):
+    y = x() + 'test'
+else:
+    z = x + 6
+
+[builtins fixtures/callable.pyi]
+
+[case testUnionMultipleReturnTypes]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, Callable[[], str], Callable[[], int]]
+
+if callable(x):
+    y = x() + 2 # E: Unsupported operand types for + (likely involving Union)
+else:
+    z = x + 6
+
+[builtins fixtures/callable.pyi]
+
+[case testUnionMultipleNonCallableTypes]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, str, Callable[[], str]]
+
+if callable(x):
+    y = x() + 'test'
+else:
+    z = x + 6  # E: Unsupported operand types for + (likely involving Union)
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableThenIsinstance]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, str, Callable[[], str], Callable[[], int]]
+
+if callable(x):
+    y = x()
+    if isinstance(y, int):
+        b1 = y + 2
+    else:
+        b2 = y + 'test'
+else:
+    if isinstance(x, int):
+        b3 = x + 3
+    else:
+        b4 = x + 'test2'
+
+[builtins fixtures/callable.pyi]
+
+[case testIsinstanceThenCallable]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, str, Callable[[], str], Callable[[], int]]
+
+if isinstance(x, int):
+    b1 = x + 1
+else:
+    if callable(x):
+        y = x()
+        if isinstance(y, int):
+            b2 = y + 1
+        else:
+            b3 = y + 'test'
+    else:
+        b4 = x + 'test2'
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableWithDifferentArgTypes]
+from typing import Callable, Union
+
+x = 5  # type: Union[int, Callable[[], None], Callable[[int], None]]
+
+if callable(x):
+    x()  # E: Too few arguments
+
+[builtins fixtures/callable.pyi]
+
+[case testClassInitializer]
+from typing import Callable, Union
+
+class A:
+    x = 5
+
+a = A  # type: Union[A, Callable[[], A]]
+
+if callable(a):
+    a = a()
+
+a.x + 6
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableVariables]
+from typing import Union
+
+class A:
+    x = 5
+
+class B:
+    x = int
+
+x = A()  # type: Union[A, B]
+
+if callable(x.x):
+    y = x.x()
+else:
+    y = x.x + 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableAnd]
+from typing import Union, Callable
+
+x = 5  # type: Union[int, Callable[[], str]]
+
+if callable(x) and x() == 'test':
+    x()
+else:
+    x + 5  # E: Unsupported left operand type for + (some union)
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableOr]
+from typing import Union, Callable
+
+x = 5  # type: Union[int, Callable[[], str]]
+
+if callable(x) or x() == 'test':  # E: "int" not callable
+    x()  # E: "int" not callable
+else:
+    x + 5
+[builtins fixtures/callable.pyi]
+
+[case testCallableOrOtherType]
+from typing import Union, Callable
+
+x = 5  # type: Union[int, Callable[[], str]]
+
+if callable(x) or x == 2:
+    pass
+else:
+    pass
+[builtins fixtures/callable.pyi]
+
+[case testAnyCallable]
+from typing import Any
+
+x = 5  # type: Any
+
+if callable(x):
+    reveal_type(x)  # E: Revealed type is 'Any'
+else:
+    reveal_type(x)  # E: Revealed type is 'Any'
+[builtins fixtures/callable.pyi]
+
+[case testCallableCallableClasses]
+from typing import Union
+
+
+class A:
+    pass
+
+
+class B:
+    def __call__(self) -> None:
+        pass
+
+
+a = A()  # type: A
+b = B()  # type: B
+c = A()  # type: Union[A, B]
+
+if callable(a):
+    5 + 'test'
+
+if not callable(b):
+    5 + 'test'
+
+if callable(c):
+    reveal_type(c)  # E: Revealed type is '__main__.B'
+else:
+    reveal_type(c)  # E: Revealed type is '__main__.A'
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableNestedUnions]
+from typing import Callable, Union
+
+T = Union[Union[int, Callable[[], int]], Union[str, Callable[[], str]]]
+
+def f(t: T) -> None:
+    if callable(t):
+        reveal_type(t())  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+    else:
+        reveal_type(t)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableTypeVarEmpty]
+from typing import TypeVar
+
+T = TypeVar('T')
+
+def f(t: T) -> T:
+    if callable(t):
+        return 5
+    else:
+        return t
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableTypeVarUnion]
+from typing import Callable, TypeVar, Union
+
+T = TypeVar('T', int, Callable[[], int], Union[str, Callable[[], str]])
+
+def f(t: T) -> None:
+    if callable(t):
+        reveal_type(t())  # E: Revealed type is 'builtins.int'  # E: Revealed type is 'builtins.str'
+    else:
+        reveal_type(t)  # E: Revealed type is 'builtins.int*'  # E: Revealed type is 'builtins.str'
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableTypeVarBound]
+from typing import TypeVar
+
+
+class A:
+    def __call__(self) -> str:
+        return 'hi'
+
+
+T = TypeVar('T', bound=A)
+
+def f(t: T) -> str:
+    if callable(t):
+        return t()
+    else:
+        return 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableTypeType]
+from typing import Type
+
+
+class A:
+    pass
+
+
+T = Type[A]
+
+def f(t: T) -> A:
+    if callable(t):
+        return t()
+    else:
+        return 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableTypeUnion]
+from abc import ABCMeta, abstractmethod
+from typing import Type, Union
+
+
+class A(metaclass=ABCMeta):
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+
+class B:
+    pass
+
+
+x = B  # type: Union[Type[A], Type[B]]
+if callable(x):
+    # Abstract classes raise an error when called, but are indeed `callable`
+    pass
+else:
+    'test' + 5
+
+[builtins fixtures/callable.pyi]
+
+[case testCallableUnionOfTypes]
+from abc import ABCMeta, abstractmethod
+from typing import Type, Union
+
+
+class A(metaclass=ABCMeta):
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+
+class B:
+    pass
+
+
+x = B  # type: Type[Union[A, B]]
+if callable(x):
+    # Abstract classes raise an error when called, but are indeed `callable`
+    pass
+else:
+    'test' + 5
+
+[builtins fixtures/callable.pyi]