-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Support TypeGuard (PEP 647) #9865
Merged
Merged
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
9469ffc
Tentative first steps for TypeGuard (PEP 647)
gvanrossum 884d9af
Rename is_type_guard to type_guard, fix repr and serialization
gvanrossum f55b284
Perhaps naive but passes the test
gvanrossum bdc97ef
Add more tests, tweak tests (one new test fails)
gvanrossum 0ed5434
Update typeshed
gvanrossum 47df17d
Make is_str_list() test work
gvanrossum fbfeeb6
Add a test showing a controversial behavior
gvanrossum 03e566f
Fix mypy and lint
gvanrossum 93387bd
Update typeshed and fix test name typo
gvanrossum 708f2e5
Sync typeshed to the version that has TypeGuard
gvanrossum cf067d1
Merge branch 'master' into typeguard
gvanrossum 0d2eb06
Add two new tests
gvanrossum 8264f8d
Minimal changes to make filter() test pass
gvanrossum 4335785
Fix mypy error in new code (corrected)
gvanrossum b34d2ac
Make methods work (adds a field to RefExpr)
gvanrossum 9bdd779
Move walrus test to 3.8-only test file
gvanrossum 249b6e5
Merge branch 'master' into typeguard
gvanrossum 639b0fc
Add cross-module test; remove test TODOs
gvanrossum 4400702
Merge remote-tracking branch 'origin/master' into typeguard
gvanrossum d108c6e
Merge remote-tracking branch 'origin/master' into typeguard
gvanrossum d96beea
Add many new tests
gvanrossum 370818f
Require that a type guard's first argument is positional
gvanrossum 9062adb
Capitalize error message
gvanrossum 5e76923
Avoid using **extra if possible
gvanrossum 37d2a5f
Clean up testTypeGuardOverload -- it still fails, though
gvanrossum 9d7b6c6
Merge remote-tracking branch 'origin/master' into typeguard
gvanrossum 896c90e
Fix lint
gvanrossum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -270,6 +270,16 @@ def copy_modified(self, *, | |
self.line, self.column) | ||
|
||
|
||
class TypeGuardType(Type): | ||
"""Only used by find_instance_check() etc.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add link to the PEP in the docstring. |
||
def __init__(self, type_guard: Type): | ||
super().__init__(line=type_guard.line, column=type_guard.column) | ||
self.type_guard = type_guard | ||
|
||
def __repr__(self) -> str: | ||
return "TypeGuard({})".format(self.type_guard) | ||
|
||
|
||
class ProperType(Type): | ||
"""Not a type alias. | ||
|
||
|
@@ -1005,6 +1015,7 @@ class CallableType(FunctionLike): | |
# tools that consume mypy ASTs | ||
'def_extras', # Information about original definition we want to serialize. | ||
# This is used for more detailed error messages. | ||
'type_guard', # T, if -> TypeGuard[T] (ret_type is bool in this case). | ||
) | ||
|
||
def __init__(self, | ||
|
@@ -1024,6 +1035,7 @@ def __init__(self, | |
from_type_type: bool = False, | ||
bound_args: Sequence[Optional[Type]] = (), | ||
def_extras: Optional[Dict[str, Any]] = None, | ||
type_guard: Optional[Type] = None, | ||
) -> None: | ||
super().__init__(line, column) | ||
assert len(arg_types) == len(arg_kinds) == len(arg_names) | ||
|
@@ -1058,6 +1070,7 @@ def __init__(self, | |
not definition.is_static else None} | ||
else: | ||
self.def_extras = {} | ||
self.type_guard = type_guard | ||
|
||
def copy_modified(self, | ||
arg_types: Bogus[Sequence[Type]] = _dummy, | ||
|
@@ -1075,7 +1088,9 @@ def copy_modified(self, | |
special_sig: Bogus[Optional[str]] = _dummy, | ||
from_type_type: Bogus[bool] = _dummy, | ||
bound_args: Bogus[List[Optional[Type]]] = _dummy, | ||
def_extras: Bogus[Dict[str, Any]] = _dummy) -> 'CallableType': | ||
def_extras: Bogus[Dict[str, Any]] = _dummy, | ||
type_guard: Bogus[Optional[Type]] = _dummy, | ||
) -> 'CallableType': | ||
return CallableType( | ||
arg_types=arg_types if arg_types is not _dummy else self.arg_types, | ||
arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds, | ||
|
@@ -1094,6 +1109,7 @@ def copy_modified(self, | |
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type, | ||
bound_args=bound_args if bound_args is not _dummy else self.bound_args, | ||
def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras), | ||
type_guard=type_guard if type_guard is not _dummy else self.type_guard, | ||
) | ||
|
||
def var_arg(self) -> Optional[FormalArgument]: | ||
|
@@ -1255,6 +1271,8 @@ def __eq__(self, other: object) -> bool: | |
def serialize(self) -> JsonDict: | ||
# TODO: As an optimization, leave out everything related to | ||
# generic functions for non-generic functions. | ||
assert (self.type_guard is None | ||
or isinstance(get_proper_type(self.type_guard), Instance)), str(self.type_guard) | ||
return {'.class': 'CallableType', | ||
'arg_types': [t.serialize() for t in self.arg_types], | ||
'arg_kinds': self.arg_kinds, | ||
|
@@ -1269,6 +1287,7 @@ def serialize(self) -> JsonDict: | |
'bound_args': [(None if t is None else t.serialize()) | ||
for t in self.bound_args], | ||
'def_extras': dict(self.def_extras), | ||
'type_guard': self.type_guard.serialize() if self.type_guard is not None else None, | ||
} | ||
|
||
@classmethod | ||
|
@@ -1286,7 +1305,9 @@ def deserialize(cls, data: JsonDict) -> 'CallableType': | |
implicit=data['implicit'], | ||
bound_args=[(None if t is None else deserialize_type(t)) | ||
for t in data['bound_args']], | ||
def_extras=data['def_extras'] | ||
def_extras=data['def_extras'], | ||
type_guard=(deserialize_type(data['type_guard']) | ||
if data['type_guard'] is not None else None), | ||
) | ||
|
||
|
||
|
@@ -2097,7 +2118,10 @@ def visit_callable_type(self, t: CallableType) -> str: | |
s = '({})'.format(s) | ||
|
||
if not isinstance(get_proper_type(t.ret_type), NoneType): | ||
s += ' -> {}'.format(t.ret_type.accept(self)) | ||
if t.type_guard is not None: | ||
s += ' -> TypeGuard[{}]'.format(t.type_guard.accept(self)) | ||
else: | ||
s += ' -> {}'.format(t.ret_type.accept(self)) | ||
|
||
if t.variables: | ||
vs = [] | ||
|
Submodule typeshed
updated
11 files
+2 −3 | stdlib/3/asyncio/events.pyi | |
+57 −88 | stdlib/3/ntpath.pyi | |
+57 −88 | stdlib/3/os/path.pyi | |
+2 −6 | stdlib/3/pathlib.pyi | |
+57 −88 | stdlib/3/posixpath.pyi | |
+7 −13 | stdlib/3/socketserver.pyi | |
+1 −0 | stdlib/3/typing.pyi | |
+1 −4 | stdlib/3/unittest/mock.pyi | |
+3 −18 | stdlib/3/venv/__init__.pyi | |
+2 −3 | stdlib/3/winreg.pyi | |
+3 −0 | third_party/2and3/typing_extensions.pyi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
[case testTypeGuardBasic] | ||
from typing_extensions import TypeGuard | ||
class Point: pass | ||
def is_point(a: object) -> TypeGuard[Point]: pass | ||
def main(a: object) -> None: | ||
if is_point(a): | ||
reveal_type(a) # N: Revealed type is '__main__.Point' | ||
else: | ||
reveal_type(a) # N: Revealed type is 'builtins.object' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardTypeArgsNone] | ||
from typing_extensions import TypeGuard | ||
def foo(a: object) -> TypeGuard: # E: TypeGuard must have exactly one type argument | ||
pass | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardTypeArgsTooMany] | ||
from typing_extensions import TypeGuard | ||
def foo(a: object) -> TypeGuard[int, int]: # E: TypeGuard must have exactly one type argument | ||
pass | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardTypeArgType] | ||
from typing_extensions import TypeGuard | ||
def foo(a: object) -> TypeGuard[42]: # E: Invalid type: try using Literal[42] instead? | ||
pass | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardRepr] | ||
from typing_extensions import TypeGuard | ||
def foo(a: object) -> TypeGuard[int]: | ||
pass | ||
reveal_type(foo) # N: Revealed type is 'def (a: builtins.object) -> TypeGuard[builtins.int]' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[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 main(a: object) -> None: | ||
if is_point(): | ||
reveal_type(a) # N: Revealed type is 'builtins.object' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardCallArgsMultiple] | ||
from typing_extensions import TypeGuard | ||
class Point: pass | ||
def is_point(a: object, b: object) -> TypeGuard[Point]: pass | ||
def main(a: object, b: object) -> None: | ||
if is_point(a, b): | ||
reveal_type(a) # N: Revealed type is '__main__.Point' | ||
reveal_type(b) # N: Revealed type is 'builtins.object' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardIsBool] | ||
from typing_extensions import TypeGuard | ||
def f(a: TypeGuard[int]) -> None: pass | ||
reveal_type(f) # N: Revealed type is 'def (a: builtins.bool)' | ||
a: TypeGuard[int] | ||
reveal_type(a) # N: Revealed type is 'builtins.bool' | ||
class C: | ||
a: TypeGuard[int] | ||
reveal_type(C().a) # N: Revealed type is 'builtins.bool' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardWithTypeVar] | ||
from typing import TypeVar, Tuple | ||
from typing_extensions import TypeGuard | ||
T = TypeVar('T') | ||
def is_two_element_tuple(a: Tuple[T, ...]) -> TypeGuard[Tuple[T, T]]: pass | ||
def main(a: Tuple[T, ...]): | ||
if is_two_element_tuple(a): | ||
reveal_type(a) # N: Revealed type is 'Tuple[T`-1, T`-1]' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardNonOverlapping] | ||
from typing import List | ||
from typing_extensions import TypeGuard | ||
def is_str_list(a: List[object]) -> TypeGuard[List[str]]: pass | ||
def main(a: List[object]): | ||
if is_str_list(a): | ||
reveal_type(a) # N: Revealed type is 'builtins.list[builtins.str]' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardUnionIn] | ||
from typing import Union | ||
from typing_extensions import TypeGuard | ||
def is_foo(a: Union[int, str]) -> TypeGuard[str]: pass | ||
def main(a: Union[str, int]) -> None: | ||
if is_foo(a): | ||
reveal_type(a) # N: Revealed type is 'builtins.str' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardUnionOut] | ||
from typing import Union | ||
from typing_extensions import TypeGuard | ||
def is_foo(a: object) -> TypeGuard[Union[int, str]]: pass | ||
def main(a: object) -> None: | ||
if is_foo(a): | ||
reveal_type(a) # N: Revealed type is 'Union[builtins.int, builtins.str]' | ||
[builtins fixtures/tuple.pyi] | ||
|
||
[case testTypeGuardNonzeroFloat] | ||
from typing import Union | ||
from typing_extensions import TypeGuard | ||
def is_nonzero(a: object) -> TypeGuard[float]: pass | ||
def main(a: int): | ||
if is_nonzero(a): | ||
reveal_type(a) # N: Revealed type is 'builtins.float' | ||
[builtins fixtures/tuple.pyi] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems fine to me, it's similar to what happens with
if isinstance()
+ you have a TODO in your tests to error at the definition site for functions that don't take args