diff --git a/mypy/checker.py b/mypy/checker.py index 64cf094558f91..37c2091d10270 100644 --- 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'] diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 1ac6b88992b89..c9ebea4dfc9b7 100644 --- 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 index 0000000000000..429ad4494e0aa --- /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] diff --git a/test-data/unit/fixtures/callable.pyi b/test-data/unit/fixtures/callable.pyi new file mode 100644 index 0000000000000..ae58648fac1ec --- /dev/null +++ b/test-data/unit/fixtures/callable.pyi @@ -0,0 +1,26 @@ +from typing import Generic, Tuple, TypeVar, Union + +T = TypeVar('T') + +class object: + def __init__(self) -> None: pass + +class type: + def __init__(self, x) -> None: pass + +class tuple(Generic[T]): pass + +class function: pass + +def isinstance(x: object, t: Union[type, Tuple[type, ...]]) -> bool: pass + +def callable(x: object) -> bool: pass + +class int: + def __add__(self, other: 'int') -> 'int': pass + def __eq__(self, other: 'int') -> 'bool': pass +class float: pass +class bool(int): pass +class str: + def __add__(self, other: 'str') -> 'str': pass + def __eq__(self, other: 'str') -> bool: pass