Skip to content

Commit 9aba159

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 f7f35e4 commit 9aba159

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
@@ -2823,7 +2823,7 @@ def function_type(self, func: FuncBase) -> FunctionLike:
28232823
return function_type(func, self.named_type('builtins.function'))
28242824

28252825
def find_isinstance_check(self, n: Expression) -> 'Tuple[TypeMap, TypeMap]':
2826-
return find_isinstance_check(n, self.type_map)
2826+
return find_isinstance_check(n, self.type_map, self)
28272827

28282828
def push_type_map(self, type_map: 'TypeMap') -> None:
28292829
if type_map is None:
@@ -2897,7 +2897,45 @@ def conditional_type_map(expr: Expression,
28972897
return {}, {}
28982898

28992899

2900-
def partition_by_callable(type: Type) -> Tuple[List[Type], List[Type]]:
2900+
def intersect_instance_callable(type: Instance, callable_type: CallableType) -> Type:
2901+
"""Creates a fake type that represents the intersection of an
2902+
Instance and a CallableType.
2903+
2904+
It operates by creating a bare-minimum dummy TypeInfo that
2905+
subclasses type and adds a __call__ method matching callable_type."""
2906+
2907+
# Build the fake ClassDef and TypeInfo together.
2908+
# The ClassDef is full of lies and doesn't actually contain a body.
2909+
cdef = ClassDef("<callable subtype of {}>".format(type.type.name()), Block([]))
2910+
info = TypeInfo(SymbolTable(), cdef, '<dummy>')
2911+
cdef.info = info
2912+
info.bases = [type]
2913+
info.calculate_mro()
2914+
2915+
# Build up a fake FuncDef so we can populate the symbol table.
2916+
func_def = FuncDef('__call__', [], Block([]), callable_type)
2917+
func_def.info = info
2918+
info.names['__call__'] = SymbolTableNode(MDEF, func_def, callable_type)
2919+
2920+
return Instance(info, [])
2921+
2922+
2923+
def make_fake_callable(type: Instance, typechecker: TypeChecker) -> Type:
2924+
"""Produce a new type that makes type Callable with a generic callable type."""
2925+
2926+
fallback = typechecker.named_type('builtins.function')
2927+
callable_type = CallableType([AnyType(TypeOfAny.explicit),
2928+
AnyType(TypeOfAny.explicit)],
2929+
[nodes.ARG_STAR, nodes.ARG_STAR2],
2930+
[None, None],
2931+
ret_type=AnyType(TypeOfAny.explicit),
2932+
fallback=fallback,
2933+
is_ellipsis_args=True)
2934+
2935+
return intersect_instance_callable(type, callable_type)
2936+
2937+
2938+
def partition_by_callable(type: Type, typechecker: TypeChecker) -> Tuple[List[Type], List[Type]]:
29012939
"""Takes in a type and partitions that type into callable subtypes and
29022940
uncallable subtypes.
29032941
@@ -2918,29 +2956,43 @@ def partition_by_callable(type: Type) -> Tuple[List[Type], List[Type]]:
29182956
callables = []
29192957
uncallables = []
29202958
for subtype in type.relevant_items():
2921-
subcallables, subuncallables = partition_by_callable(subtype)
2959+
subcallables, subuncallables = partition_by_callable(subtype, typechecker)
29222960
callables.extend(subcallables)
29232961
uncallables.extend(subuncallables)
29242962
return callables, uncallables
29252963

29262964
if isinstance(type, TypeVarType):
2927-
return partition_by_callable(type.erase_to_union_or_bound())
2965+
# We could do better probably?
2966+
# Refine the the type variable's bound as our type in the case that
2967+
# callable() is true. This unfortuantely loses the information that
2968+
# the type is a type variable in that branch.
2969+
# This matches what is done for isinstance, but it may be possible to
2970+
# do better.
2971+
# If it is possible for the false branch to execute, return the original
2972+
# type to avoid losing type information.
2973+
callables, uncallables = partition_by_callable(type.erase_to_union_or_bound(), typechecker)
2974+
uncallables = [type] if len(uncallables) else []
2975+
return callables, uncallables
29282976

29292977
if isinstance(type, Instance):
29302978
method = type.type.get_method('__call__')
29312979
if method and method.type:
2932-
callables, uncallables = partition_by_callable(method.type)
2980+
callables, uncallables = partition_by_callable(method.type, typechecker)
29332981
if len(callables) and not len(uncallables):
29342982
# Only consider the type callable if its __call__ method is
29352983
# definitely callable.
29362984
return [type], []
2937-
return [], [type]
29382985

2939-
return [], [type]
2986+
ret = make_fake_callable(type, typechecker)
2987+
return [ret], [type]
2988+
2989+
# We don't know how properly make the type callable.
2990+
return [type], [type]
29402991

29412992

29422993
def conditional_callable_type_map(expr: Expression,
29432994
current_type: Optional[Type],
2995+
typechecker: TypeChecker,
29442996
) -> Tuple[TypeMap, TypeMap]:
29452997
"""Takes in an expression and the current type of the expression.
29462998
@@ -2954,7 +3006,7 @@ def conditional_callable_type_map(expr: Expression,
29543006
if isinstance(current_type, AnyType):
29553007
return {}, {}
29563008

2957-
callables, uncallables = partition_by_callable(current_type)
3009+
callables, uncallables = partition_by_callable(current_type, typechecker)
29583010

29593011
if len(callables) and len(uncallables):
29603012
callable_map = {expr: UnionType.make_union(callables)} if len(callables) else None
@@ -3083,6 +3135,7 @@ def convert_to_typetype(type_map: TypeMap) -> TypeMap:
30833135

30843136
def find_isinstance_check(node: Expression,
30853137
type_map: Dict[Expression, Type],
3138+
typechecker: TypeChecker,
30863139
) -> Tuple[TypeMap, TypeMap]:
30873140
"""Find any isinstance checks (within a chain of ands). Includes
30883141
implicit and explicit checks for None and calls to callable.
@@ -3136,7 +3189,7 @@ def find_isinstance_check(node: Expression,
31363189
expr = node.args[0]
31373190
if literal(expr) == LITERAL_TYPE:
31383191
vartype = type_map[expr]
3139-
return conditional_callable_type_map(expr, vartype)
3192+
return conditional_callable_type_map(expr, vartype, typechecker)
31403193
elif isinstance(node, ComparisonExpr) and experiments.STRICT_OPTIONAL:
31413194
# Check for `x is None` and `x is not None`.
31423195
is_not = node.operators == ['is not']
@@ -3196,23 +3249,23 @@ def find_isinstance_check(node: Expression,
31963249
else_map = {ref: else_type} if not isinstance(else_type, UninhabitedType) else None
31973250
return if_map, else_map
31983251
elif isinstance(node, OpExpr) and node.op == 'and':
3199-
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map)
3200-
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map)
3252+
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map, typechecker)
3253+
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map, typechecker)
32013254

32023255
# (e1 and e2) is true if both e1 and e2 are true,
32033256
# and false if at least one of e1 and e2 is false.
32043257
return (and_conditional_maps(left_if_vars, right_if_vars),
32053258
or_conditional_maps(left_else_vars, right_else_vars))
32063259
elif isinstance(node, OpExpr) and node.op == 'or':
3207-
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map)
3208-
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map)
3260+
left_if_vars, left_else_vars = find_isinstance_check(node.left, type_map, typechecker)
3261+
right_if_vars, right_else_vars = find_isinstance_check(node.right, type_map, typechecker)
32093262

32103263
# (e1 or e2) is true if at least one of e1 or e2 is true,
32113264
# and false if both e1 and e2 are false.
32123265
return (or_conditional_maps(left_if_vars, right_if_vars),
32133266
and_conditional_maps(left_else_vars, right_else_vars))
32143267
elif isinstance(node, UnaryExpr) and node.op == 'not':
3215-
left, right = find_isinstance_check(node.expr, type_map)
3268+
left, right = find_isinstance_check(node.expr, type_map, typechecker)
32163269
return right, left
32173270

32183271
# Not a supported isinstance check

mypy/checkexpr.py

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

22902290
# values are only part of the comprehension when all conditions are true
2291-
true_map, _ = mypy.checker.find_isinstance_check(condition, self.chk.type_map)
2291+
true_map, _ = mypy.checker.find_isinstance_check(condition, self.chk.type_map,
2292+
self.chk)
22922293

22932294
if true_map:
22942295
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)