-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
'in' can narrow TypedDict unions #13838
Changes from 18 commits
ca87360
2a73325
9671a69
0b3701b
1f4224a
e67c6f8
88e2c9f
f799344
20a2e66
b4dc248
88f03f6
123656c
a47bc04
f72a634
bf4364f
062fcb1
a57f580
2807feb
520df60
620da98
fb564eb
178483e
26c4d04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2025,6 +2025,193 @@ v = {bad2: 2} # E: Extra key "bad" for TypedDict "Value" | |||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testOperatorContainsNarrowsTypedDicts_unionWithList] | ||||||||||||||
from __future__ import annotations | ||||||||||||||
from typing import assert_type, TypedDict, Union | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class D(TypedDict): | ||||||||||||||
foo: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
d: D | list[str] | ||||||||||||||
|
||||||||||||||
if 'foo' in d: | ||||||||||||||
assert_type(d, Union[D, list[str]]) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, list[str]) | ||||||||||||||
|
||||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testOperatorContainsNarrowsTypedDicts_total] | ||||||||||||||
from __future__ import annotations | ||||||||||||||
from typing import assert_type, Literal, TypedDict, TypeVar, Union | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class D1(TypedDict): | ||||||||||||||
foo: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class D2(TypedDict): | ||||||||||||||
bar: int | ||||||||||||||
|
||||||||||||||
d_or_list: D1 | list[str] | ||||||||||||||
|
||||||||||||||
if 'foo' in d_or_list: | ||||||||||||||
assert_type(d_or_list, Union[D1, list[str]]) | ||||||||||||||
else: | ||||||||||||||
assert_type(d_or_list, list[str]) | ||||||||||||||
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.
Suggested change
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. |
||||||||||||||
|
||||||||||||||
d: D1 | D2 | ||||||||||||||
|
||||||||||||||
if 'foo' in d: | ||||||||||||||
assert_type(d, D1) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, D2) | ||||||||||||||
|
||||||||||||||
foo_or_bar: Literal['foo', 'bar'] | ||||||||||||||
if foo_or_bar in d: | ||||||||||||||
assert_type(d, Union[D1, D2]) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[D1, D2]) | ||||||||||||||
|
||||||||||||||
foo_or_invalid: Literal['foo', 'invalid'] | ||||||||||||||
if foo_or_invalid in d: | ||||||||||||||
assert_type(d, D1) | ||||||||||||||
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. in theory this could narrow 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. That'd have to be implemented. And I think it could be pretty neat, but would require a rework: I'd have to pass in the left expression, and return type maps (not "if_type" and "else_type"). Maybe in a follow-up? 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. My current implementation is to make a form of "tagged TypeDicts" work, but I suspect a more generalized form of type narrowing should be possible. However, before we tackle the more contrived (I might be naive, though, and this might've been tried before and proven impossible.) P.S. PyRight is similarly limited in this 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. Oh sorry, to be clear my suggestion wasn't to implement this / I don't think it's a terribly important feature. I was just saying it's worth adding an 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. Gotcha, done: fb564eb |
||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[D1, D2]) | ||||||||||||||
|
||||||||||||||
TD = TypeVar('TD', D1, D2) | ||||||||||||||
|
||||||||||||||
def f(arg: TD) -> None: | ||||||||||||||
value: int | ||||||||||||||
if 'foo' in arg: | ||||||||||||||
assert_type(d, Union[D1, D2]) # strangely enough it's seen as a union | ||||||||||||||
value = arg['foo'] # but acts here as D1 | ||||||||||||||
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. I think this is because mypy processes these twice. Want to change the test to just do 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. |
||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[D1, D2]) # ditto here, but D2 | ||||||||||||||
value = arg['bar'] | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testOperatorContainsNarrowsTypedDicts_final] | ||||||||||||||
# flags: --warn-unreachable | ||||||||||||||
from __future__ import annotations | ||||||||||||||
from typing import assert_type, TypedDict, Union | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class DFinal(TypedDict): | ||||||||||||||
foo: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class DNotFinal(TypedDict): | ||||||||||||||
bar: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
d_not_final: DNotFinal | ||||||||||||||
|
||||||||||||||
if 'bar' in d_not_final: | ||||||||||||||
assert_type(d_not_final, DNotFinal) | ||||||||||||||
else: | ||||||||||||||
spam = 'ham' # E: Statement is unreachable | ||||||||||||||
|
||||||||||||||
if 'spam' in d_not_final: | ||||||||||||||
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. What will happen for 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. Added test, thanks. (Yes, it does that.) 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. |
||||||||||||||
assert_type(d_not_final, DNotFinal) | ||||||||||||||
else: | ||||||||||||||
assert_type(d_not_final, DNotFinal) | ||||||||||||||
|
||||||||||||||
d_final: DFinal | ||||||||||||||
|
||||||||||||||
if 'spam' in d_final: | ||||||||||||||
spam = 'ham' # E: Statement is unreachable | ||||||||||||||
else: | ||||||||||||||
assert_type(d_final, DFinal) | ||||||||||||||
|
||||||||||||||
d_union: DFinal | DNotFinal | ||||||||||||||
|
||||||||||||||
if 'foo' in d_union: | ||||||||||||||
assert_type(d_union, Union[DFinal, DNotFinal]) | ||||||||||||||
else: | ||||||||||||||
assert_type(d_union, DNotFinal) | ||||||||||||||
|
||||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse] | ||||||||||||||
from __future__ import annotations | ||||||||||||||
from typing import assert_type, Literal, TypedDict, Union | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class DTotal(TypedDict): | ||||||||||||||
required_key: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class DNotTotal(TypedDict, total=False): | ||||||||||||||
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. Let's also test 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. I can do it. I assumed another test assured it's equivalent, but I can explicitly test it. 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. |
||||||||||||||
optional_key: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
d: DTotal | DNotTotal | ||||||||||||||
|
||||||||||||||
if 'required_key' in d: | ||||||||||||||
assert_type(d, DTotal) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, DNotTotal) | ||||||||||||||
|
||||||||||||||
if 'optional_key' in d: | ||||||||||||||
assert_type(d, DNotTotal) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[DTotal, DNotTotal]) | ||||||||||||||
|
||||||||||||||
key: Literal['optional_key', 'required_key'] | ||||||||||||||
if key in d: | ||||||||||||||
assert_type(d, Union[DTotal, DNotTotal]) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[DTotal, DNotTotal]) | ||||||||||||||
|
||||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired] | ||||||||||||||
from __future__ import annotations | ||||||||||||||
from typing import assert_type, Required, NotRequired, TypedDict, Union | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class D1(TypedDict): | ||||||||||||||
required_key: Required[int] | ||||||||||||||
optional_key: NotRequired[int] | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@final | ||||||||||||||
class D2(TypedDict): | ||||||||||||||
abc: int | ||||||||||||||
xyz: int | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
d: D1 | D2 | ||||||||||||||
|
||||||||||||||
if 'required_key' in d: | ||||||||||||||
assert_type(d, D1) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, D2) | ||||||||||||||
|
||||||||||||||
if 'optional_key' in d: | ||||||||||||||
assert_type(d, D1) | ||||||||||||||
else: | ||||||||||||||
assert_type(d, Union[D1, D2]) | ||||||||||||||
|
||||||||||||||
[builtins fixtures/dict.pyi] | ||||||||||||||
[typing fixtures/typing-typeddict.pyi] | ||||||||||||||
|
||||||||||||||
[case testCannotSubclassFinalTypedDict] | ||||||||||||||
from typing import TypedDict | ||||||||||||||
from typing_extensions import final | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
from abc import ABCMeta | ||
|
||
cast = 0 | ||
assert_type = 0 | ||
overload = 0 | ||
Any = 0 | ||
Union = 0 | ||
|
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 test case seems completely duplicated in testOperatorContainsNarrowsTypedDicts_total, let's remove this?
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.
I'll remove the relevant part from
testOperatorContainsNarrowsTypedDicts_total
, as to keep test names descriptive.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.
520df60