diff --git a/mypy/checker.py b/mypy/checker.py index dfbfa753d5f2..afe7c774090a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6183,21 +6183,29 @@ def find_isinstance_check_helper( attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1])) if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1: return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0]) - elif isinstance(node.callee, RefExpr): - if node.callee.type_guard is not None or node.callee.type_is is not None: + elif isinstance(node.callee, (RefExpr, CallExpr)): + # We support both named callables (RefExpr) and temporaries (CallExpr). + # For temporaries (e.g., E()(x)), we extract type_is/type_guard from the __call__ method. + # For named callables (e.g., is_int(x)), we extract type_is/type_guard directly from the RefExpr. + type_is, type_guard = None, None + try: + called_type = get_proper_type(self.lookup_type(node.callee)) + except KeyError: + called_type = None + # TODO: there are some more cases in check_call() to handle. + # If the callee is an instance, try to extract TypeGuard/TypeIs from its __call__ method. + if called_type and isinstance(called_type, Instance): + call = find_member("__call__", called_type, called_type, is_operator=True) + if call is not None: + called_type = get_proper_type(call) + if isinstance(called_type, CallableType): + type_is, type_guard = called_type.type_is, called_type.type_guard + # If the callee is a RefExpr, extract TypeGuard/TypeIs directly. + if isinstance(node.callee, RefExpr): + type_is, type_guard = node.callee.type_is, node.callee.type_guard + if type_guard is not None or type_is is not None: # TODO: Follow *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: - # the first argument might be used as a kwarg - called_type = get_proper_type(self.lookup_type(node.callee)) - - # TODO: there are some more cases in check_call() to handle. - if isinstance(called_type, Instance): - call = find_member( - "__call__", called_type, called_type, is_operator=True - ) - if call is not None: - called_type = get_proper_type(call) - # *assuming* the overloaded function is correct, there's a couple cases: # 1) The first argument has different names, but is pos-only. We don't # care about this case, the argument must be passed positionally. @@ -6210,9 +6218,7 @@ def find_isinstance_check_helper( # we want the idx-th variable to be narrowed expr = collapse_walrus(node.args[idx]) else: - kind = ( - "guard" if node.callee.type_guard is not None else "narrower" - ) + kind = "guard" if type_guard is not None else "narrower" self.fail( message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node ) @@ -6223,15 +6229,15 @@ def find_isinstance_check_helper( # considered "always right" (i.e. even if the types are not overlapping). # Also note that a care must be taken to unwrap this back at read places # where we use this to narrow down declared type. - if node.callee.type_guard is not None: - return {expr: TypeGuardedType(node.callee.type_guard)}, {} + if type_guard is not None: + return {expr: TypeGuardedType(type_guard)}, {} else: - assert node.callee.type_is is not None + assert type_is is not None return conditional_types_to_typemaps( expr, *self.conditional_types_with_intersection( self.lookup_type(expr), - [TypeRange(node.callee.type_is, is_upper_bound=False)], + [TypeRange(type_is, is_upper_bound=False)], expr, consider_runtime_isinstance=False, ), diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index fdcfcc969adc..93e665e4548c 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -731,6 +731,53 @@ assert a(x=x) reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] +# https://github.com/python/mypy/issues/19575 +[case testNoCrashOnDunderCallTypeGuardTemporaryObject] +from typing_extensions import TypeGuard +class E: + def __init__(self) -> None: ... + def __call__(self, o: object) -> TypeGuard[int]: + return True +x = object() +if E()(x): + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testNoCrashOnDunderCallTypeIsTemporaryObject] +from typing_extensions import TypeIs +class E: + def __init__(self) -> None: ... + def __call__(self, o: object) -> TypeIs[int]: + return True +x = object() +if E()(x): + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testNoCrashOnDunderCallTypeIsTemporaryObjectGeneric] +from typing import Generic, TypeVar +from typing_extensions import TypeIs +T = TypeVar("T") +class E(Generic[T]): + def __init__(self) -> None: ... + def __call__(self, o: object) -> TypeIs[T]: + return True +x = object() +if E[int]()(x): + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testTypeGuardTemporaryObjectWithKeywordArg] +from typing_extensions import TypeGuard +class E: + def __init__(self) -> None: ... + def __call__(self, o: object) -> TypeGuard[int]: + return True +x = object() +if E()(o=x): + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + [case testTypeGuardRestrictAwaySingleInvariant] from typing import List from typing_extensions import TypeGuard