Skip to content

Commit 47d2ea5

Browse files
authored
Changes how TypeGuard types are checked in subtypes (#11314)
Fixes #11307
1 parent 3f43c83 commit 47d2ea5

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

mypy/messages.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,10 @@ def format(typ: Type) -> str:
17531753
# return type (this always works).
17541754
return format(TypeType.make_normalized(erase_type(func.items[0].ret_type)))
17551755
elif isinstance(func, CallableType):
1756-
return_type = format(func.ret_type)
1756+
if func.type_guard is not None:
1757+
return_type = f'TypeGuard[{format(func.type_guard)}]'
1758+
else:
1759+
return_type = format(func.ret_type)
17571760
if func.is_ellipsis_args:
17581761
return 'Callable[..., {}]'.format(return_type)
17591762
arg_strings = []

mypy/subtypes.py

+7
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ def visit_type_var(self, left: TypeVarType) -> bool:
310310
def visit_callable_type(self, left: CallableType) -> bool:
311311
right = self.right
312312
if isinstance(right, CallableType):
313+
if left.type_guard is not None and right.type_guard is not None:
314+
if not self._is_subtype(left.type_guard, right.type_guard):
315+
return False
316+
elif right.type_guard is not None and left.type_guard is None:
317+
# This means that one function has `TypeGuard` and other does not.
318+
# They are not compatible. See https://github.com/python/mypy/issues/11307
319+
return False
313320
return is_callable_compatible(
314321
left, right,
315322
is_compat=self._is_subtype,

test-data/unit/check-typeguard.test

+89
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,92 @@ def foobar_typeguard(x: object):
458458
return
459459
reveal_type(x) # N: Revealed type is "__main__.<subclass of "Foo" and "Bar">"
460460
[builtins fixtures/tuple.pyi]
461+
462+
[case testTypeGuardAsFunctionArgAsBoolSubtype]
463+
from typing import Callable
464+
from typing_extensions import TypeGuard
465+
466+
def accepts_bool(f: Callable[[object], bool]): pass
467+
468+
def with_bool_typeguard(o: object) -> TypeGuard[bool]: pass
469+
def with_str_typeguard(o: object) -> TypeGuard[str]: pass
470+
def with_bool(o: object) -> bool: pass
471+
472+
accepts_bool(with_bool_typeguard)
473+
accepts_bool(with_str_typeguard)
474+
accepts_bool(with_bool)
475+
[builtins fixtures/tuple.pyi]
476+
477+
[case testTypeGuardAsFunctionArg]
478+
from typing import Callable
479+
from typing_extensions import TypeGuard
480+
481+
def accepts_typeguard(f: Callable[[object], TypeGuard[bool]]): pass
482+
def different_typeguard(f: Callable[[object], TypeGuard[str]]): pass
483+
484+
def with_typeguard(o: object) -> TypeGuard[bool]: pass
485+
def with_bool(o: object) -> bool: pass
486+
487+
accepts_typeguard(with_typeguard)
488+
accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[bool]]"
489+
490+
different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeGuard[bool]]"; expected "Callable[[object], TypeGuard[str]]"
491+
different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[str]]"
492+
[builtins fixtures/tuple.pyi]
493+
494+
[case testTypeGuardAsGenericFunctionArg]
495+
from typing import Callable, TypeVar
496+
from typing_extensions import TypeGuard
497+
498+
T = TypeVar('T')
499+
500+
def accepts_typeguard(f: Callable[[object], TypeGuard[T]]): pass
501+
502+
def with_bool_typeguard(o: object) -> TypeGuard[bool]: pass
503+
def with_str_typeguard(o: object) -> TypeGuard[str]: pass
504+
def with_bool(o: object) -> bool: pass
505+
506+
accepts_typeguard(with_bool_typeguard)
507+
accepts_typeguard(with_str_typeguard)
508+
accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[bool]]"
509+
[builtins fixtures/tuple.pyi]
510+
511+
[case testTypeGuardAsOverloadedFunctionArg]
512+
# https://github.com/python/mypy/issues/11307
513+
from typing import Callable, TypeVar, Generic, Any, overload
514+
from typing_extensions import TypeGuard
515+
516+
_T = TypeVar('_T')
517+
518+
class filter(Generic[_T]):
519+
@overload
520+
def __init__(self, function: Callable[[object], TypeGuard[_T]]) -> None: pass
521+
@overload
522+
def __init__(self, function: Callable[[_T], Any]) -> None: pass
523+
def __init__(self, function): pass
524+
525+
def is_int_typeguard(a: object) -> TypeGuard[int]: pass
526+
def returns_bool(a: object) -> bool: pass
527+
528+
reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int*]"
529+
reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object*]"
530+
[builtins fixtures/tuple.pyi]
531+
532+
[case testTypeGuardSubtypingVariance]
533+
from typing import Callable
534+
from typing_extensions import TypeGuard
535+
536+
class A: pass
537+
class B(A): pass
538+
class C(B): pass
539+
540+
def accepts_typeguard(f: Callable[[object], TypeGuard[B]]): pass
541+
542+
def with_typeguard_a(o: object) -> TypeGuard[A]: pass
543+
def with_typeguard_b(o: object) -> TypeGuard[B]: pass
544+
def with_typeguard_c(o: object) -> TypeGuard[C]: pass
545+
546+
accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeGuard[A]]"; expected "Callable[[object], TypeGuard[B]]"
547+
accepts_typeguard(with_typeguard_b)
548+
accepts_typeguard(with_typeguard_c)
549+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)