From a29a6ec49add9c1849853fb3d12551f00a3bc5d6 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 3 Dec 2022 13:49:33 +0900 Subject: [PATCH 1/7] The specification requires positional arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > Type checkers should assume that type narrowing should be applied to the expression that is passed as the first positional argument to a user-defined type guard. [...] > > If a type guard function is implemented as an instance method or class method, the first positional argument maps to the second parameter (after “self” or “cls”). --- mypy/semanal.py | 13 ++++++++++++ test-data/unit/check-typeguard.test | 33 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 266dc891b697..902b08d9f1f9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -865,6 +865,19 @@ def analyze_func_def(self, defn: FuncDef) -> None: return assert isinstance(result, ProperType) if isinstance(result, CallableType): + # type guards need to have a positional argument, to spec + if ( + result.type_guard + and ARG_POS not in result.arg_kinds[self.is_class_scope() :] + ): + self.fail( + "TypeGuard functions must have a positional argument", + result, + code=codes.VALID_TYPE, + ) + # in this case, we just kind of just ... remove the type guard. + result = result.copy_modified(type_guard=None) + result = self.remove_unpack_kwargs(defn, result) if has_self_type and self.type is not None: info = self.type diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index cf72e7033087..e1f1cd81d928 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -37,8 +37,8 @@ reveal_type(foo) # N: Revealed type is "def (a: builtins.object) -> TypeGuard[b [case testTypeGuardCallArgsNone] from typing_extensions import TypeGuard class Point: pass -# TODO: error on the 'def' line (insufficient args for type guard) -def is_point() -> TypeGuard[Point]: pass + +def is_point() -> TypeGuard[Point]: pass # E: TypeGuard functions must have a positional argument def main(a: object) -> None: if is_point(): reveal_type(a) # N: Revealed type is "builtins.object" @@ -597,3 +597,32 @@ def func(names: Tuple[str, ...]): if is_two_element_tuple(names): reveal_type(names) # N: Revealed type is "Tuple[builtins.str, builtins.str]" [builtins fixtures/tuple.pyi] + +[case testTypeGuardErroneousDefinitionFails] +from typing_extensions import TypeGuard + +class Z: + def typeguard(self, *, x: object) -> TypeGuard[int]: # E: TypeGuard functions must have a positional argument + ... + +def bad_typeguard(*, x: object) -> TypeGuard[int]: # E: TypeGuard functions must have a positional argument + ... +[builtins fixtures/tuple.pyi] + +[case testTypeGuardWithKeywordArg-xfail] +from typing_extensions import TypeGuard + +class Z: + def typeguard(self, x: object) -> TypeGuard[int]: + ... + +def typeguard(x: object) -> TypeGuard[int]: + ... + +n: object +if typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" + +if Z().typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] From 3dda329c1665f2c662770b88077e21dbe3e21281 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 3 Dec 2022 14:44:45 +0900 Subject: [PATCH 2/7] Start allowing keyword arguments to type guards --- mypy/checker.py | 15 ++++++++++++--- test-data/unit/check-typeguard.test | 12 ++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1c8956ae6722..0f660c180745 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5284,10 +5284,19 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM 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: - # TODO: Follow keyword args or *args, **kwargs + # TODO: Follow *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: - self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED, node) - return {}, {} + # the first argument might be used as a kwarg + called_type = get_proper_type(self.lookup_type(node.callee)) + assert isinstance(called_type, CallableType) + name = called_type.arg_names[0] + if name in node.arg_names: + idx = node.arg_names.index(name) + # we want the idx-th variable to be narrowed + expr = collapse_walrus(node.args[idx]) + else: + self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED, node) + return {}, {} if literal(expr) == LITERAL_TYPE: # Note: we wrap the target type, so that we can special case later. # Namely, for isinstance() we use a normal meet, while TypeGuard is diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index e1f1cd81d928..cce957518dde 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -227,13 +227,13 @@ def main(a: object) -> None: from typing_extensions import TypeGuard def is_float(a: object, b: object = 0) -> TypeGuard[float]: pass def main1(a: object) -> None: - # This is debatable -- should we support these cases? + if is_float(a=a, b=1): + reveal_type(a) # N: Revealed type is "builtins.float" - if is_float(a=a, b=1): # E: Type guard requires positional argument - reveal_type(a) # N: Revealed type is "builtins.object" + if is_float(b=1, a=a): + reveal_type(a) # N: Revealed type is "builtins.float" - if is_float(b=1, a=a): # E: Type guard requires positional argument - reveal_type(a) # N: Revealed type is "builtins.object" + # This is debatable -- should we support these cases? ta = (a,) if is_float(*ta): # E: Type guard requires positional argument @@ -609,7 +609,7 @@ def bad_typeguard(*, x: object) -> TypeGuard[int]: # E: TypeGuard functions mus ... [builtins fixtures/tuple.pyi] -[case testTypeGuardWithKeywordArg-xfail] +[case testTypeGuardWithKeywordArg] from typing_extensions import TypeGuard class Z: From 10c7e3fbb7ac894f2427369567b195d32138ac76 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 3 Dec 2022 15:38:10 +0900 Subject: [PATCH 3/7] Special case staticmethod --- mypy/semanal.py | 1 + test-data/unit/check-typeguard.test | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 902b08d9f1f9..003c93dcbfbd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -869,6 +869,7 @@ def analyze_func_def(self, defn: FuncDef) -> None: if ( result.type_guard and ARG_POS not in result.arg_kinds[self.is_class_scope() :] + and not defn.is_static ): self.fail( "TypeGuard functions must have a positional argument", diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index cce957518dde..84a7440beb5a 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -607,7 +607,21 @@ class Z: def bad_typeguard(*, x: object) -> TypeGuard[int]: # E: TypeGuard functions must have a positional argument ... + +# make sure not to break other things + +class Y: + @staticmethod + def typeguard(h: object) -> TypeGuard[int]: + ... + +x: object +if Y().typeguard(x): + reveal_type(x) # N: Revealed type is "builtins.int" +if Y.typeguard(x): + reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] +[builtins fixtures/classmethod.pyi] [case testTypeGuardWithKeywordArg] from typing_extensions import TypeGuard From 3ea4baf3c1ee4de5c4fecc872e36ab35e585cf89 Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Mon, 5 Dec 2022 14:07:32 +0000 Subject: [PATCH 4/7] PR review --- mypy/checker.py | 11 ++++- test-data/unit/check-typeguard.test | 75 ++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0f660c180745..07ac8a80c6fd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5288,8 +5288,15 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM 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)) - assert isinstance(called_type, CallableType) - name = called_type.arg_names[0] + assert isinstance(called_type, (CallableType, Overloaded)) + + # *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. + # 2) The first argument allows keyword reference, therefore must be the + # same between overloads. + name = called_type.items[0].arg_names[0] + if name in node.arg_names: idx = node.arg_names.index(name) # we want the idx-th variable to be narrowed diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 84a7440beb5a..fb81e86dfbe8 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -607,8 +607,50 @@ class Z: def bad_typeguard(*, x: object) -> TypeGuard[int]: # E: TypeGuard functions must have a positional argument ... +[builtins fixtures/tuple.pyi] + +[case testTypeGuardWithKeywordArg] +from typing_extensions import TypeGuard + +class Z: + def typeguard(self, x: object) -> TypeGuard[int]: + ... + +def typeguard(x: object) -> TypeGuard[int]: + ... + +n: object +if typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" + +if Z().typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testTypeGuardWithPositionalOnlyArg] +from typing_extensions import TypeGuard + +def typeguard(x: object, /) -> TypeGuard[int]: + ... + +n: object +if typeguard(n): + reveal_type(n) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testTypeGuardKeywordFollowingWalrus] +from typing import cast +from typing_extensions import TypeGuard + +def typeguard(x: object) -> TypeGuard[int]: + ... + +if typeguard(x=(n := cast(object, "hi"))): + reveal_type(n) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] -# make sure not to break other things +[case testStaticMethodTypeGuard] +from typing_extensions import TypeGuard class Y: @staticmethod @@ -623,20 +665,31 @@ if Y.typeguard(x): [builtins fixtures/tuple.pyi] [builtins fixtures/classmethod.pyi] -[case testTypeGuardWithKeywordArg] +[case testTypeGuardKwargFollowingThroughOverloaded] +from typing import overload, Union from typing_extensions import TypeGuard -class Z: - def typeguard(self, x: object) -> TypeGuard[int]: - ... +@overload +def typeguard(x: object, y: str) -> TypeGuard[str]: + ... -def typeguard(x: object) -> TypeGuard[int]: +@overload +def typeguard(x: object, y: int) -> TypeGuard[int]: ... -n: object -if typeguard(x=n): - reveal_type(n) # N: Revealed type is "builtins.int" +def typeguard(x: object, y: Union[int, str]) -> Union[TypeGuard[int], TypeGuard[str]]: + ... -if Z().typeguard(x=n): - reveal_type(n) # N: Revealed type is "builtins.int" +x: object +if typeguard(x=x, y=42): + reveal_type(x) # N: Revealed type is "builtins.int" + +if typeguard(y=42, x=x): + reveal_type(x) # N: Revealed type is "builtins.int" + +if typeguard(x=x, y="42"): + reveal_type(x) # N: Revealed type is "builtins.str" + +if typeguard(y="42", x=x): + reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/tuple.pyi] From 9c9a791ac89bd6aecd2f42972e7fc27155828375 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 8 Dec 2022 13:52:32 +0900 Subject: [PATCH 5/7] Fix tests --- test-data/unit/check-typeguard.test | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index fb81e86dfbe8..fce929046b3a 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -628,6 +628,7 @@ if Z().typeguard(x=n): [builtins fixtures/tuple.pyi] [case testTypeGuardWithPositionalOnlyArg] +# flags: --python-version 3.8 from typing_extensions import TypeGuard def typeguard(x: object, /) -> TypeGuard[int]: @@ -635,10 +636,15 @@ def typeguard(x: object, /) -> TypeGuard[int]: n: object if typeguard(n): - reveal_type(n) # N: Revealed type is "builtins.int" + reveal_type(n) [builtins fixtures/tuple.pyi] +[out] +main:4: error: invalid syntax +[out version>=3.8] +main:9: note: Revealed type is "builtins.int" [case testTypeGuardKeywordFollowingWalrus] +# flags: --python-version 3.8 from typing import cast from typing_extensions import TypeGuard @@ -646,8 +652,12 @@ def typeguard(x: object) -> TypeGuard[int]: ... if typeguard(x=(n := cast(object, "hi"))): - reveal_type(n) # N: Revealed type is "builtins.int" + reveal_type(n) [builtins fixtures/tuple.pyi] +[out] +main:8: error: invalid syntax +[out version>=3.8] +main:9: note: Revealed type is "builtins.int" [case testStaticMethodTypeGuard] from typing_extensions import TypeGuard From 43adc46dc81a39d59dd8fb197c77b4d085220fa2 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 10 Dec 2022 14:49:32 +0900 Subject: [PATCH 6/7] Small test fixes --- test-data/unit/check-typeguard.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index fce929046b3a..493dbb4c111c 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -639,7 +639,7 @@ if typeguard(n): reveal_type(n) [builtins fixtures/tuple.pyi] [out] -main:4: error: invalid syntax +main:4: error: invalid syntax; you likely need to run mypy using Python 3.8 or newer [out version>=3.8] main:9: note: Revealed type is "builtins.int" @@ -655,7 +655,7 @@ if typeguard(x=(n := cast(object, "hi"))): reveal_type(n) [builtins fixtures/tuple.pyi] [out] -main:8: error: invalid syntax +main:8: error: invalid syntax; you likely need to run mypy using Python 3.8 or newer [out version>=3.8] main:9: note: Revealed type is "builtins.int" From c6f55d194af849ef9920d5bbe72fb81b274d7d77 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 11 Dec 2022 15:16:28 +0900 Subject: [PATCH 7/7] Move Python 3.8 tests to check-python38 --- test-data/unit/check-python38.test | 28 +++++++++++++++++++++++++ test-data/unit/check-typeguard.test | 32 ----------------------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index 30bdadf900c3..9fce9ef4c419 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -734,3 +734,31 @@ class C(Generic[T]): [out] main:10: note: Revealed type is "builtins.int" main:10: note: Revealed type is "builtins.str" + +[case testTypeGuardWithPositionalOnlyArg] +# flags: --python-version 3.8 +from typing_extensions import TypeGuard + +def typeguard(x: object, /) -> TypeGuard[int]: + ... + +n: object +if typeguard(n): + reveal_type(n) +[builtins fixtures/tuple.pyi] +[out] +main:9: note: Revealed type is "builtins.int" + +[case testTypeGuardKeywordFollowingWalrus] +# flags: --python-version 3.8 +from typing import cast +from typing_extensions import TypeGuard + +def typeguard(x: object) -> TypeGuard[int]: + ... + +if typeguard(x=(n := cast(object, "hi"))): + reveal_type(n) +[builtins fixtures/tuple.pyi] +[out] +main:9: note: Revealed type is "builtins.int" diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 493dbb4c111c..39bcb091f09e 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -627,38 +627,6 @@ if Z().typeguard(x=n): reveal_type(n) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeGuardWithPositionalOnlyArg] -# flags: --python-version 3.8 -from typing_extensions import TypeGuard - -def typeguard(x: object, /) -> TypeGuard[int]: - ... - -n: object -if typeguard(n): - reveal_type(n) -[builtins fixtures/tuple.pyi] -[out] -main:4: error: invalid syntax; you likely need to run mypy using Python 3.8 or newer -[out version>=3.8] -main:9: note: Revealed type is "builtins.int" - -[case testTypeGuardKeywordFollowingWalrus] -# flags: --python-version 3.8 -from typing import cast -from typing_extensions import TypeGuard - -def typeguard(x: object) -> TypeGuard[int]: - ... - -if typeguard(x=(n := cast(object, "hi"))): - reveal_type(n) -[builtins fixtures/tuple.pyi] -[out] -main:8: error: invalid syntax; you likely need to run mypy using Python 3.8 or newer -[out version>=3.8] -main:9: note: Revealed type is "builtins.int" - [case testStaticMethodTypeGuard] from typing_extensions import TypeGuard