Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed spec change about TypeGuard/bool compatibility #1637

Merged
merged 4 commits into from Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions conformance/results/mypy/narrowing_typeguard.toml
Expand Up @@ -2,4 +2,6 @@ conformant = "Pass"
output = """
narrowing_typeguard.py:102: error: TypeGuard functions must have a positional argument [valid-type]
narrowing_typeguard.py:107: error: TypeGuard functions must have a positional argument [valid-type]
narrowing_typeguard.py:128: error: Argument 1 to "takes_callable_str" has incompatible type "Callable[[object], TypeGuard[int]]"; expected "Callable[[object], str]" [arg-type]
narrowing_typeguard.py:148: error: Argument 1 to "takes_callable_str_proto" has incompatible type "Callable[[object], TypeGuard[int]]"; expected "CallableStrProto" [arg-type]
"""
2 changes: 1 addition & 1 deletion conformance/results/mypy/version.toml
@@ -1,2 +1,2 @@
version = "mypy 1.8.0"
test_duration = 1.4
test_duration = 1.5
2 changes: 2 additions & 0 deletions conformance/results/pyre/narrowing_typeguard.toml
Expand Up @@ -6,4 +6,6 @@ Does not reject TypeGuard method with too few parameters.
output = """
narrowing_typeguard.py:17:33 Incompatible parameter type [6]: In call `typing.GenericMeta.__getitem__`, for 1st positional argument, expected `Type[Variable[_T_co](covariant)]` but got `Tuple[Type[str], Type[str]]`.
narrowing_typeguard.py:19:33 Incompatible parameter type [6]: In call `typing.GenericMeta.__getitem__`, for 1st positional argument, expected `Type[Variable[_T_co](covariant)]` but got `Tuple[Type[str], typing.Any]`.
narrowing_typeguard.py:128:19 Incompatible parameter type [6]: In call `takes_callable_str`, for 1st positional argument, expected `typing.Callable[[object], str]` but got `typing.Callable(simple_typeguard)[[Named(val, object)], TypeGuard[int]]`.
narrowing_typeguard.py:148:25 Incompatible parameter type [6]: In call `takes_callable_str_proto`, for 1st positional argument, expected `CallableStrProto` but got `typing.Callable(simple_typeguard)[[Named(val, object)], TypeGuard[int]]`.
"""
2 changes: 1 addition & 1 deletion conformance/results/pyre/version.toml
@@ -1,2 +1,2 @@
version = "pyre 0.9.19"
test_duration = 3.0
test_duration = 3.1
10 changes: 10 additions & 0 deletions conformance/results/pyright/narrowing_typeguard.toml
Expand Up @@ -2,4 +2,14 @@ conformant = "Pass"
output = """
narrowing_typeguard.py:102:9 - error: User-defined type guard functions and methods must have at least one input parameter (reportGeneralTypeIssues)
narrowing_typeguard.py:107:9 - error: User-defined type guard functions and methods must have at least one input parameter (reportGeneralTypeIssues)
narrowing_typeguard.py:128:20 - error: Argument of type "(val: object) -> TypeGuard[int]" cannot be assigned to parameter "f" of type "(object) -> str" in function "takes_callable_str"
  Type "(val: object) -> TypeGuard[int]" cannot be assigned to type "(object) -> str"
    Function return type "TypeGuard[int]" is incompatible with type "str"
      "TypeGuard[int]" is incompatible with "str"
      "bool" is incompatible with "str" (reportArgumentType)
narrowing_typeguard.py:148:26 - error: Argument of type "(val: object) -> TypeGuard[int]" cannot be assigned to parameter "f" of type "CallableStrProto" in function "takes_callable_str_proto"
  Type "(val: object) -> TypeGuard[int]" cannot be assigned to type "(val: object) -> str"
    Function return type "TypeGuard[int]" is incompatible with type "str"
      "TypeGuard[int]" is incompatible with "str"
      "bool" is incompatible with "str" (reportArgumentType)
"""
2 changes: 1 addition & 1 deletion conformance/results/pyright/version.toml
@@ -1,2 +1,2 @@
version = "pyright 1.1.351"
test_duration = 1.4
test_duration = 1.6
3 changes: 2 additions & 1 deletion conformance/results/pytype/narrowing_typeguard.toml
Expand Up @@ -3,5 +3,6 @@ notes = """
Does not reject TypeGuard method with too few parameters.
"""
output = """

File "narrowing_typeguard.py", line 128, in <module>: Function takes_callable_str was called with the wrong arguments [wrong-arg-types]
File "narrowing_typeguard.py", line 148, in <module>: Function takes_callable_str_proto was called with the wrong arguments [wrong-arg-types]
"""
2 changes: 1 addition & 1 deletion conformance/results/pytype/version.toml
@@ -1,2 +1,2 @@
version = "pytype 2024.02.13"
test_duration = 32.5
test_duration = 42.5
2 changes: 1 addition & 1 deletion conformance/results/results.html

Large diffs are not rendered by default.

50 changes: 45 additions & 5 deletions conformance/tests/narrowing_typeguard.py
Expand Up @@ -4,7 +4,7 @@

# Specification: https://typing.readthedocs.io/en/latest/spec/narrowing.html#typeguard

from typing import Any, Self, TypeGuard, TypeVar, assert_type
from typing import Any, Callable, Protocol, Self, TypeGuard, TypeVar, assert_type


T = TypeVar("T")
Expand Down Expand Up @@ -39,18 +39,18 @@ def func2(val: set[object]):
class A:
def tg_1(self, val: object) -> TypeGuard[int]:
return isinstance(val, int)

@classmethod
def tg_2(cls, val: object) -> TypeGuard[int]:
return isinstance(val, int)

@staticmethod
def tg_3(val: object) -> TypeGuard[int]:
return isinstance(val, int)

def tg4(self, val: object) -> TypeGuard[Self]:
return isinstance(val, type(self))

def tg5(self: T_A, val: object) -> TypeGuard[T_A]:
return isinstance(val, type(self))

Expand Down Expand Up @@ -101,8 +101,48 @@ class C:
# Type checker should emit error here.
def tg_1(self) -> TypeGuard[int]:
return False

@classmethod
# Type checker should emit error here.
def tg_2(cls) -> TypeGuard[int]:
return False

# > ``TypeGuard`` is also valid as the return type of a ``Callable`` type. In that
# > context, it is treated as a subtype of bool. For example, ``Callable[..., TypeGuard[int]]``
# > is assignable to ``Callable[..., bool]``.


def takes_callable_bool(f: Callable[[object], bool]) -> None:
pass


def takes_callable_str(f: Callable[[object], str]) -> None:
pass


def simple_typeguard(val: object) -> TypeGuard[int]:
return isinstance(val, int)


takes_callable_bool(simple_typeguard) # OK
takes_callable_str(simple_typeguard) # Error


class CallableBoolProto(Protocol):
def __call__(self, val: object) -> bool: ...


class CallableStrProto(Protocol):
def __call__(self, val: object) -> str: ...


def takes_callable_bool_proto(f: CallableBoolProto) -> None:
pass


def takes_callable_str_proto(f: CallableStrProto) -> None:
pass


takes_callable_bool_proto(simple_typeguard) # OK
takes_callable_str_proto(simple_typeguard) # Error
7 changes: 4 additions & 3 deletions docs/spec/narrowing.rst
Expand Up @@ -19,9 +19,10 @@ user-defined type guard function. Return statements within a type guard function
should return bool values, and type checkers should verify that all return paths
return a bool.

In all other respects, ``TypeGuard`` is a distinct type from bool. It is not a
subtype of bool. Therefore, ``Callable[..., TypeGuard[int]]`` is not assignable
to ``Callable[..., bool]``.
``TypeGuard`` is also valid as the return type of a callable, for example
in callback protocols and in the ``Callable`` special form. In these
contexts, it is treated as a subtype of bool. For example, ``Callable[..., TypeGuard[int]]``
is assignable to ``Callable[..., bool]``.

When ``TypeGuard`` is used to annotate the return type of a function or
method that accepts at least one parameter, that function or method is
Expand Down