Skip to content

Commit 3df1996

Browse files
committed
Rework how we refine information from callable()
Don't ever assume that something is *not* callable; basically anything could be, and getting it wrong leaves code to not be typechecked. When operating on an Instance that is not clearly callable, construct a callable version of it by generating a fake subclass with a __call__ method to be used. Fixes #3605
1 parent 01de89f commit 3df1996

File tree

3 files changed

+100
-20
lines changed

3 files changed

+100
-20
lines changed

mypy/checker.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2809,7 +2809,7 @@ def function_type(self, func: FuncBase) -> FunctionLike:
28092809
return function_type(func, self.named_type('builtins.function'))
28102810

28112811
def find_isinstance_check(self, n: Expression) -> 'Tuple[TypeMap, TypeMap]':
2812-
return find_isinstance_check(n, self.type_map)
2812+
return find_isinstance_check(n, self.type_map, self)
28132813

28142814
def push_type_map(self, type_map: 'TypeMap') -> None:
28152815
if type_map is None:
@@ -2883,7 +2883,45 @@ def conditional_type_map(expr: Expression,
28832883
return {}, {}
28842884

28852885

2886-
def partition_by_callable(type: Type) -> Tuple[List[Type], List[Type]]:
2886+
def intersect_instance_callable(type: Instance, callable_type: CallableType) -> Type:
2887+
"""Creates a fake type that represents the intersection of an
2888+
Instance and a CallableType.
2889+
2890+
It operates by creating a bare-minimum dummy TypeInfo that
2891+
subclasses type and adds a __call__ method matching callable_type."""
2892+
2893+
# Build the fake ClassDef and TypeInfo together.
2894+
# The ClassDef is full of lies and doesn't actually contain a body.
2895+
cdef = ClassDef("<callable subtype of {}>".format(type.type.name()), Block([]))
2896+
info = TypeInfo(SymbolTable(), cdef, '<dummy>')
2897+
cdef.info = info
2898+
info.bases = [type]
2899+
info.calculate_mro()
2900+
2901+
# Build up a fake FuncDef so we can populate the symbol table.
2902+
func_def = FuncDef('__call__', [], Block([]), callable_type)
2903+
func_def.info = info
2904+
info.names['__call__'] = SymbolTableNode(MDEF, func_def, callable_type)
2905+
2906+
return Instance(info, [])
2907+
2908+
2909+
def make_fake_callable(type: Instance, typechecker: TypeChecker) -> Type:
2910+
"""Produce a new type that makes type Callable with a generic callable type."""
2911+
2912+
fallback = typechecker.named_type('builtins.function')
2913+
callable_type = CallableType([AnyType(TypeOfAny.explicit),
2914+
AnyType(TypeOfAny.explicit)],
2915+
[nodes.ARG_STAR, nodes.ARG_STAR2],
2916+
[None, None],
2917+
ret_type=AnyType(TypeOfAny.explicit),
2918+
fallback=fallback,
2919+
is_ellipsis_args=True)
2920+
2921+
return intersect_instance_callable(type, callable_type)
2922+
2923+
2924+
def partition_by_callable(type: Type, typechecker: TypeChecker) -> Tuple[List[Type], List[Type]]:
28872925
"""Takes in a type and partitions that type into callable subtypes and
28882926
uncallable subtypes.
28892927
@@ -2904,29 +2942,43 @@ def partition_by_callable(type: Type) -> Tuple[List[Type], List[Type]]:
29042942
callables = []
29052943
uncallables = []
29062944
for subtype in type.relevant_items():
2907-
subcallables, subuncallables = partition_by_callable(subtype)
2945+
subcallables, subuncallables = partition_by_callable(subtype, typechecker)
29082946
callables.extend(subcallables)
29092947
uncallables.extend(subuncallables)
29102948
return callables, uncallables
29112949

29122950
if isinstance(type, TypeVarType):
2913-
return partition_by_callable(type.erase_to_union_or_bound())
2951+
# We could do better probably?
2952+
# Refine the the type variable's bound as our type in the case that
2953+
# callable() is true. This unfortuantely loses the information that
2954+
# the type is a type variable in that branch.
2955+
# This matches what is done for isinstance, but it may be possible to
2956+
# do better.
2957+
# If it is possible for the false branch to execute, return the original
2958+
# type to avoid losing type information.
2959+
callables, uncallables = partition_by_callable(type.erase_to_union_or_bound(), typechecker)
2960+
uncallables = [type] if len(uncallables) else []
2961+
return callables, uncallables
29142962

29152963
if isinstance(type, Instance):
29162964
method = type.type.get_method('__call__')
29172965
if method and method.type:
2918-
callables, uncallables = partition_by_callable(method.type)
2966+
callables, uncallables = partition_by_callable(method.type, typechecker)
29192967
if len(callables) and not len(uncallables):
29202968
# Only consider the type callable if its __call__ method is
29212969
# definitely callable.
29222970
return [type], []
2923-
return [], [type]
29242971

2925-
return [], [type]
2972+
ret = make_fake_callable(type, typechecker)
2973+
return [ret], [type]
2974+
2975+
# We don't know how properly make the type callable.
2976+
return [type], [type]
29262977

29272978

29282979
def conditional_callable_type_map(expr: Expression,
29292980
current_type: Optional[Type],
2981+
typechecker: TypeChecker,
29302982
) -> Tuple[TypeMap, TypeMap]:
29312983
"""Takes in an expression and the current type of the expression.
29322984
@@ -2940,7 +2992,7 @@ def conditional_callable_type_map(expr: Expression,
29402992
if isinstance(current_type, AnyType):
29412993
return {}, {}
29422994

2943-
callables, uncallables = partition_by_callable(current_type)
2995+
callables, uncallables = partition_by_callable(current_type, typechecker)
29442996

29452997
if len(callables) and len(uncallables):
29462998
callable_map = {expr: UnionType.make_union(callables)} if len(callables) else None
@@ -3069,6 +3121,7 @@ def convert_to_typetype(type_map: TypeMap) -> TypeMap:
30693121

30703122
def find_isinstance_check(node: Expression,
30713123
type_map: Dict[Expression, Type],
3124+
typechecker: TypeChecker,
30723125
) -> Tuple[TypeMap, TypeMap]:
30733126
"""Find any isinstance checks (within a chain of ands). Includes
30743127
implicit and explicit checks for None and calls to callable.
@@ -3122,7 +3175,7 @@ def find_isinstance_check(node: Expression,
31223175
expr = node.args[0]
31233176
if literal(expr) == LITERAL_TYPE:
31243177
vartype = type_map[expr]
3125-
return conditional_callable_type_map(expr, vartype)
3178+
return conditional_callable_type_map(expr, vartype, typechecker)
31263179
elif isinstance(node, ComparisonExpr) and experiments.STRICT_OPTIONAL:
31273180
# Check for `x is None` and `x is not None`.
31283181
is_not = node.operators == ['is not']
@@ -3182,23 +3235,23 @@ def find_isinstance_check(node: Expression,
31823235
else_map = {ref: else_type} if not isinstance(else_type, UninhabitedType) else None
31833236
return if_map, else_map
31843237
elif isinstance(node, OpExpr) and node.op == 'and':
3185-
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map)
3186-
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map)
3238+
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map, typechecker)
3239+
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map, typechecker)
31873240

31883241
# (e1 and e2) is true if both e1 and e2 are true,
31893242
# and false if at least one of e1 and e2 is false.
31903243
return (and_conditional_maps(left_if_vars, right_if_vars),
31913244
or_conditional_maps(left_else_vars, right_else_vars))
31923245
elif isinstance(node, OpExpr) and node.op == 'or':
3193-
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map)
3194-
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map)
3246+
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map, typechecker)
3247+
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map, typechecker)
31953248

31963249
# (e1 or e2) is true if at least one of e1 or e2 is true,
31973250
# and false if both e1 and e2 are false.
31983251
return (or_conditional_maps(left_if_vars, right_if_vars),
31993252
and_conditional_maps(left_else_vars, right_else_vars))
32003253
elif isinstance(node, UnaryExpr) and node.op == 'not':
3201-
left, right = find_isinstance_check(node.expr, type_map)
3254+
left, right = find_isinstance_check(node.expr, type_map, typechecker)
32023255
return right, left
32033256

32043257
# Not a supported isinstance check

mypy/checkexpr.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2284,7 +2284,8 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No
22842284
self.accept(condition)
22852285

22862286
# values are only part of the comprehension when all conditions are true
2287-
true_map, _ = mypy.checker.find_isinstance_check(condition, self.chk.type_map)
2287+
true_map, _ = mypy.checker.find_isinstance_check(condition, self.chk.type_map,
2288+
self.chk)
22882289

22892290
if true_map:
22902291
for var, type in true_map.items():

test-data/unit/check-callable.test

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,13 @@ b = B() # type: B
208208
c = A() # type: Union[A, B]
209209

210210
if callable(a):
211-
5 + 'test'
211+
5 + 'test' # E: Unsupported operand types for + ("int" and "str")
212212

213213
if not callable(b):
214214
5 + 'test'
215215

216216
if callable(c):
217-
reveal_type(c) # E: Revealed type is '__main__.B'
217+
reveal_type(c) # E: Revealed type is 'Union[<callable subtype of A>, __main__.B]'
218218
else:
219219
reveal_type(c) # E: Revealed type is '__main__.A'
220220

@@ -227,7 +227,7 @@ T = Union[Union[int, Callable[[], int]], Union[str, Callable[[], str]]]
227227

228228
def f(t: T) -> None:
229229
if callable(t):
230-
reveal_type(t()) # E: Revealed type is 'Union[builtins.int, builtins.str]'
230+
reveal_type(t()) # E: Revealed type is 'Union[Any, builtins.int, builtins.str]'
231231
else:
232232
reveal_type(t) # E: Revealed type is 'Union[builtins.int, builtins.str]'
233233

@@ -240,7 +240,7 @@ T = TypeVar('T')
240240

241241
def f(t: T) -> T:
242242
if callable(t):
243-
return 5
243+
return 5 # E: Incompatible return value type (got "int", expected "T")
244244
else:
245245
return t
246246

@@ -253,7 +253,7 @@ T = TypeVar('T', int, Callable[[], int], Union[str, Callable[[], str]])
253253

254254
def f(t: T) -> None:
255255
if callable(t):
256-
reveal_type(t()) # E: Revealed type is 'builtins.int' # E: Revealed type is 'builtins.str'
256+
reveal_type(t()) # E: Revealed type is 'Any' # E: Revealed type is 'builtins.int' # E: Revealed type is 'Union[Any, builtins.str]'
257257
else:
258258
reveal_type(t) # E: Revealed type is 'builtins.int*' # E: Revealed type is 'builtins.str'
259259

@@ -343,3 +343,29 @@ else:
343343
'test' + 5
344344

345345
[builtins fixtures/callable.pyi]
346+
347+
[case testCallableObject]
348+
349+
def f(o: object) -> None:
350+
if callable(o):
351+
o()
352+
1 + 'boom' # E: Unsupported operand types for + ("int" and "str")
353+
o()
354+
355+
[builtins fixtures/callable.pyi]
356+
357+
[case testCallableObject2]
358+
359+
class Foo(object):
360+
def bar(self) -> None:
361+
pass
362+
363+
def g(o: Foo) -> None:
364+
o.bar()
365+
if callable(o):
366+
o.bar()
367+
o()
368+
else:
369+
o.bar()
370+
371+
[builtins fixtures/callable.pyi]

0 commit comments

Comments
 (0)