From a985400c27cf492f91eb566637b2e89e0de4284c Mon Sep 17 00:00:00 2001 From: Alex Frieder Date: Sat, 31 Dec 2016 23:53:53 -0500 Subject: [PATCH 1/4] Introduces conditional type inference based on callable --- mypy/checker.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 64cf094558f91..b502121890965 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2506,6 +2506,68 @@ def conditional_type_map(expr: Expression, return {}, {} +def is_callable_type(type: Optional[Type]) -> bool: + """Takes in a type and returns if that type is callable, that is, + if `type()` is well-typed (ignoring arguments).""" + callable_types = (FunctionLike, AnyType, TypeType) + if any(isinstance(type, callable_type) for callable_type in callable_types): + return True + + if isinstance(type, UnionType): + return all(is_callable_type(item) for item in type.items) + + if isinstance(type, TypeVarType): + return not is_callable_type(type.erase_to_union_or_bound()) + + if isinstance(type, Instance): + method = type.type.get_method('__call__') + if method: + return is_callable_type(method.type) + + return False + + +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 must be callable. The second element is a + map from the expression to the type it would hold if it weren't + callable. Considers AnyType to be both callable and uncallable.""" + if not current_type: + return {}, {} + + if isinstance(current_type, AnyType): + return {}, {} + + if isinstance(current_type, UnionType): + callables = [] # type: List[Type] + uncallables = [] # type: List[Type] + for item in current_type.items: + + if isinstance(item, AnyType): + # We want Any to be considered both callable and uncallable + callables.append(item) + uncallables.append(item) + + else: + if is_callable_type(item): + callables.append(item) + else: + uncallables.append(item) + + 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 + + if is_callable_type(current_type): + 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 +2641,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 +2662,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'] From 25c73d038105ff25cef08cd181bb84b60ec39e93 Mon Sep 17 00:00:00 2001 From: Alex Frieder Date: Sun, 1 Jan 2017 00:57:44 -0500 Subject: [PATCH 2/4] Add tests for callable type-checker --- mypy/test/testcheck.py | 1 + test-data/unit/check-callable.test | 221 +++++++++++++++++++++++++++ test-data/unit/fixtures/callable.pyi | 26 ++++ 3 files changed, 248 insertions(+) create mode 100644 test-data/unit/check-callable.test create mode 100644 test-data/unit/fixtures/callable.pyi 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..49fdc13b2c3dd --- /dev/null +++ b/test-data/unit/check-callable.test @@ -0,0 +1,221 @@ +[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] 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 From 4dd7b905f1b9b87607ca0d910850d9ec402023b3 Mon Sep 17 00:00:00 2001 From: Alex Frieder Date: Tue, 10 Jan 2017 21:34:18 -0500 Subject: [PATCH 3/4] Rewrote callable checking to work properly --- mypy/checker.py | 66 +++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b502121890965..37c2091d10270 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2506,63 +2506,71 @@ def conditional_type_map(expr: Expression, return {}, {} -def is_callable_type(type: Optional[Type]) -> bool: - """Takes in a type and returns if that type is callable, that is, - if `type()` is well-typed (ignoring arguments).""" - callable_types = (FunctionLike, AnyType, TypeType) - if any(isinstance(type, callable_type) for callable_type in callable_types): - return True +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): - return all(is_callable_type(item) for item in type.items) + 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 not is_callable_type(type.erase_to_union_or_bound()) + return partition_by_callable(type.erase_to_union_or_bound()) if isinstance(type, Instance): method = type.type.get_method('__call__') if method: - return is_callable_type(method.type) + 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 False + 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 + """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 must be callable. The second element is a + 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. Considers AnyType to be both callable and uncallable.""" + callable.""" if not current_type: return {}, {} if isinstance(current_type, AnyType): return {}, {} - if isinstance(current_type, UnionType): - callables = [] # type: List[Type] - uncallables = [] # type: List[Type] - for item in current_type.items: - - if isinstance(item, AnyType): - # We want Any to be considered both callable and uncallable - callables.append(item) - uncallables.append(item) - - else: - if is_callable_type(item): - callables.append(item) - else: - uncallables.append(item) + 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 - if is_callable_type(current_type): + elif len(callables): return {}, None return None, {} From de057d137d8f48ba5cb96bbf7f109a9960f70022 Mon Sep 17 00:00:00 2001 From: Alex Frieder Date: Tue, 10 Jan 2017 22:07:58 -0500 Subject: [PATCH 4/4] Added more tests --- test-data/unit/check-callable.test | 124 +++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/test-data/unit/check-callable.test b/test-data/unit/check-callable.test index 49fdc13b2c3dd..429ad4494e0aa 100644 --- a/test-data/unit/check-callable.test +++ b/test-data/unit/check-callable.test @@ -219,3 +219,127 @@ 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]