From 0515b52fe6fe21408a5af057bc13d08f15418e7a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 11 Oct 2021 14:23:32 +0300 Subject: [PATCH 1/3] Changes how `TypeGuard` types are checked in subtypes, refs #11307 --- mypy/messages.py | 5 ++- mypy/subtypes.py | 7 ++++ test-data/unit/check-typeguard.test | 55 +++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/mypy/messages.py b/mypy/messages.py index 086f3d22aee1..8499e5eceee3 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1740,7 +1740,10 @@ def format(typ: Type) -> str: # return type (this always works). return format(TypeType.make_normalized(erase_type(func.items[0].ret_type))) elif isinstance(func, CallableType): - return_type = format(func.ret_type) + if func.type_guard is not None: + return_type = f'TypeGuard[{format(func.type_guard)}]' + else: + return_type = format(func.ret_type) if func.is_ellipsis_args: return 'Callable[..., {}]'.format(return_type) arg_strings = [] diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 52d03282494f..971278e1f721 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -310,6 +310,13 @@ def visit_type_var(self, left: TypeVarType) -> bool: def visit_callable_type(self, left: CallableType) -> bool: right = self.right if isinstance(right, CallableType): + if left.type_guard is not None and right.type_guard is not None: + if not self._is_subtype(left.type_guard, right.type_guard): + return False + elif bool(left.type_guard) != bool(right.type_guard): + # This means that one function has `TypeGuard` and other does not. + # They are not compatible. See https://github.com/python/mypy/issues/11307 + return False return is_callable_compatible( left, right, is_compat=self._is_subtype, diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index fb26f0d3d537..fcfbb8d30f0d 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -458,3 +458,58 @@ def foobar_typeguard(x: object): return reveal_type(x) # N: Revealed type is "__main__." [builtins fixtures/tuple.pyi] + +[case testTypeGuardAsFunctionArg] +from typing import Callable +from typing_extensions import TypeGuard + +def accepts_typeguard(f: Callable[[object], TypeGuard[bool]]): pass +def different_typeguard(f: Callable[[object], TypeGuard[str]]): pass + +def with_typeguard(o: object) -> TypeGuard[bool]: pass +def with_bool(o: object) -> bool: pass + +accepts_typeguard(with_typeguard) +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[bool]]" + +different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeGuard[bool]]"; expected "Callable[[object], TypeGuard[str]]" +different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[str]]" +[builtins fixtures/tuple.pyi] + +[case testTypeGuardAsGenericFunctionArg] +from typing import Callable, TypeVar +from typing_extensions import TypeGuard + +T = TypeVar('T') + +def accepts_typeguard(f: Callable[[object], TypeGuard[T]]): pass + +def with_bool_typeguard(o: object) -> TypeGuard[bool]: pass +def with_str_typeguard(o: object) -> TypeGuard[str]: pass +def with_bool(o: object) -> bool: pass + +accepts_typeguard(with_bool_typeguard) +accepts_typeguard(with_str_typeguard) +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[bool]]" +[builtins fixtures/tuple.pyi] + +[case testTypeGuardAsOverloadedFunctionArg] +# https://github.com/python/mypy/issues/11307 +from typing import Callable, TypeVar, Generic, Any, overload +from typing_extensions import TypeGuard + +_T = TypeVar('_T') + +class filter(Generic[_T]): + @overload + def __init__(self, function: Callable[[object], TypeGuard[_T]]) -> None: pass + @overload + def __init__(self, function: Callable[[_T], Any]) -> None: pass + def __init__(self, function): pass + +def is_int_typeguard(a: object) -> TypeGuard[int]: pass +def returns_bool(a: object) -> bool: pass + +reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int*]" +reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object*]" +[builtins fixtures/tuple.pyi] From f5d4429aaddbce08f641e552161dbfa619af5b79 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 11 Oct 2021 14:36:11 +0300 Subject: [PATCH 2/3] Changes how `TypeGuard` types are checked in subtypes, refs #11307 --- mypy/subtypes.py | 2 +- test-data/unit/check-typeguard.test | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 971278e1f721..8f804481104f 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -313,7 +313,7 @@ def visit_callable_type(self, left: CallableType) -> bool: if left.type_guard is not None and right.type_guard is not None: if not self._is_subtype(left.type_guard, right.type_guard): return False - elif bool(left.type_guard) != bool(right.type_guard): + elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. # They are not compatible. See https://github.com/python/mypy/issues/11307 return False diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index fcfbb8d30f0d..00d993fac37f 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -459,6 +459,21 @@ def foobar_typeguard(x: object): reveal_type(x) # N: Revealed type is "__main__." [builtins fixtures/tuple.pyi] +[case testTypeGuardAsFunctionArgAsBoolSubtype] +from typing import Callable +from typing_extensions import TypeGuard + +def accepts_bool(f: Callable[[object], bool]): pass + +def with_bool_typeguard(o: object) -> TypeGuard[bool]: pass +def with_str_typeguard(o: object) -> TypeGuard[str]: pass +def with_bool(o: object) -> bool: pass + +accepts_bool(with_bool_typeguard) +accepts_bool(with_str_typeguard) +accepts_bool(with_bool) +[builtins fixtures/tuple.pyi] + [case testTypeGuardAsFunctionArg] from typing import Callable from typing_extensions import TypeGuard From 8536e6dbf4bf02f46fa0f3b8583d2a6743c63308 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 13 Oct 2021 23:04:44 +0300 Subject: [PATCH 3/3] Addresses review --- test-data/unit/check-typeguard.test | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 00d993fac37f..6dae011b59ce 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -528,3 +528,22 @@ def returns_bool(a: object) -> bool: pass reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int*]" reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object*]" [builtins fixtures/tuple.pyi] + +[case testTypeGuardSubtypingVariance] +from typing import Callable +from typing_extensions import TypeGuard + +class A: pass +class B(A): pass +class C(B): pass + +def accepts_typeguard(f: Callable[[object], TypeGuard[B]]): pass + +def with_typeguard_a(o: object) -> TypeGuard[A]: pass +def with_typeguard_b(o: object) -> TypeGuard[B]: pass +def with_typeguard_c(o: object) -> TypeGuard[C]: pass + +accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeGuard[A]]"; expected "Callable[[object], TypeGuard[B]]" +accepts_typeguard(with_typeguard_b) +accepts_typeguard(with_typeguard_c) +[builtins fixtures/tuple.pyi]