Skip to content

Commit de6e742

Browse files
Fix TypeGuard with call on temporary object (#19577)
Fixes #19575 by adding support for TypeGaurd/TypeIs when they are used on methods off of classes which were not saved to a variable. Solution adapted from copilot answer here and then refined: saulshanabrook#1
1 parent a6bfb2e commit de6e742

File tree

2 files changed

+70
-20
lines changed

2 files changed

+70
-20
lines changed

mypy/checker.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6218,21 +6218,26 @@ def find_isinstance_check_helper(
62186218
attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
62196219
if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
62206220
return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
6221-
elif isinstance(node.callee, RefExpr):
6222-
if node.callee.type_guard is not None or node.callee.type_is is not None:
6221+
else:
6222+
type_is, type_guard = None, None
6223+
called_type = self.lookup_type_or_none(node.callee)
6224+
if called_type is not None:
6225+
called_type = get_proper_type(called_type)
6226+
# TODO: there are some more cases in check_call() to handle.
6227+
# If the callee is an instance, try to extract TypeGuard/TypeIs from its __call__ method.
6228+
if isinstance(called_type, Instance):
6229+
call = find_member("__call__", called_type, called_type, is_operator=True)
6230+
if call is not None:
6231+
called_type = get_proper_type(call)
6232+
if isinstance(called_type, CallableType):
6233+
type_is, type_guard = called_type.type_is, called_type.type_guard
6234+
6235+
# If the callee is a RefExpr, extract TypeGuard/TypeIs directly.
6236+
if isinstance(node.callee, RefExpr):
6237+
type_is, type_guard = node.callee.type_is, node.callee.type_guard
6238+
if type_guard is not None or type_is is not None:
62236239
# TODO: Follow *args, **kwargs
62246240
if node.arg_kinds[0] != nodes.ARG_POS:
6225-
# the first argument might be used as a kwarg
6226-
called_type = get_proper_type(self.lookup_type(node.callee))
6227-
6228-
# TODO: there are some more cases in check_call() to handle.
6229-
if isinstance(called_type, Instance):
6230-
call = find_member(
6231-
"__call__", called_type, called_type, is_operator=True
6232-
)
6233-
if call is not None:
6234-
called_type = get_proper_type(call)
6235-
62366241
# *assuming* the overloaded function is correct, there's a couple cases:
62376242
# 1) The first argument has different names, but is pos-only. We don't
62386243
# care about this case, the argument must be passed positionally.
@@ -6245,9 +6250,7 @@ def find_isinstance_check_helper(
62456250
# we want the idx-th variable to be narrowed
62466251
expr = collapse_walrus(node.args[idx])
62476252
else:
6248-
kind = (
6249-
"guard" if node.callee.type_guard is not None else "narrower"
6250-
)
6253+
kind = "guard" if type_guard is not None else "narrower"
62516254
self.fail(
62526255
message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node
62536256
)
@@ -6258,15 +6261,15 @@ def find_isinstance_check_helper(
62586261
# considered "always right" (i.e. even if the types are not overlapping).
62596262
# Also note that a care must be taken to unwrap this back at read places
62606263
# where we use this to narrow down declared type.
6261-
if node.callee.type_guard is not None:
6262-
return {expr: TypeGuardedType(node.callee.type_guard)}, {}
6264+
if type_guard is not None:
6265+
return {expr: TypeGuardedType(type_guard)}, {}
62636266
else:
6264-
assert node.callee.type_is is not None
6267+
assert type_is is not None
62656268
return conditional_types_to_typemaps(
62666269
expr,
62676270
*self.conditional_types_with_intersection(
62686271
self.lookup_type(expr),
6269-
[TypeRange(node.callee.type_is, is_upper_bound=False)],
6272+
[TypeRange(type_is, is_upper_bound=False)],
62706273
expr,
62716274
consider_runtime_isinstance=False,
62726275
),

test-data/unit/check-typeguard.test

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,53 @@ assert a(x=x)
731731
reveal_type(x) # N: Revealed type is "builtins.int"
732732
[builtins fixtures/tuple.pyi]
733733

734+
# https://github.com/python/mypy/issues/19575
735+
[case testNoCrashOnDunderCallTypeGuardTemporaryObject]
736+
from typing_extensions import TypeGuard
737+
class E:
738+
def __init__(self) -> None: ...
739+
def __call__(self, o: object) -> TypeGuard[int]:
740+
return True
741+
x = object()
742+
if E()(x):
743+
reveal_type(x) # N: Revealed type is "builtins.int"
744+
[builtins fixtures/tuple.pyi]
745+
746+
[case testNoCrashOnDunderCallTypeIsTemporaryObject]
747+
from typing_extensions import TypeIs
748+
class E:
749+
def __init__(self) -> None: ...
750+
def __call__(self, o: object) -> TypeIs[int]:
751+
return True
752+
x = object()
753+
if E()(x):
754+
reveal_type(x) # N: Revealed type is "builtins.int"
755+
[builtins fixtures/tuple.pyi]
756+
757+
[case testNoCrashOnDunderCallTypeIsTemporaryObjectGeneric]
758+
from typing import Generic, TypeVar
759+
from typing_extensions import TypeIs
760+
T = TypeVar("T")
761+
class E(Generic[T]):
762+
def __init__(self) -> None: ...
763+
def __call__(self, o: object) -> TypeIs[T]:
764+
return True
765+
x = object()
766+
if E[int]()(x):
767+
reveal_type(x) # N: Revealed type is "builtins.int"
768+
[builtins fixtures/tuple.pyi]
769+
770+
[case testTypeGuardTemporaryObjectWithKeywordArg]
771+
from typing_extensions import TypeGuard
772+
class E:
773+
def __init__(self) -> None: ...
774+
def __call__(self, o: object) -> TypeGuard[int]:
775+
return True
776+
x = object()
777+
if E()(o=x):
778+
reveal_type(x) # N: Revealed type is "builtins.int"
779+
[builtins fixtures/tuple.pyi]
780+
734781
[case testTypeGuardRestrictAwaySingleInvariant]
735782
from typing import List
736783
from typing_extensions import TypeGuard

0 commit comments

Comments
 (0)