-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Do not treat match value patterns as isinstance checks
#20146
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
Changes from all commits
67ded4a
a028936
f4a478b
443a359
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 |
|---|---|---|
|
|
@@ -30,9 +30,9 @@ m: Any | |
|
|
||
| match m: | ||
| case 1: | ||
| reveal_type(m) # N: Revealed type is "Literal[1]" | ||
| reveal_type(m) # N: Revealed type is "Any" | ||
| case 2: | ||
| reveal_type(m) # N: Revealed type is "Literal[2]" | ||
| reveal_type(m) # N: Revealed type is "Any" | ||
| case other: | ||
| reveal_type(other) # N: Revealed type is "Any" | ||
|
|
||
|
|
@@ -61,7 +61,7 @@ m: object | |
|
|
||
| match m: | ||
| case b.b: | ||
| reveal_type(m) # N: Revealed type is "builtins.int" | ||
| reveal_type(m) # N: Revealed type is "builtins.object" | ||
| [file b.py] | ||
| b: int | ||
|
|
||
|
|
@@ -83,7 +83,7 @@ m: A | |
|
|
||
| match m: | ||
| case b.b: | ||
| reveal_type(m) # N: Revealed type is "__main__.<subclass of "__main__.A" and "b.B">" | ||
| reveal_type(m) # N: Revealed type is "__main__.A" | ||
sterliakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [file b.py] | ||
| class B: ... | ||
| b: B | ||
|
|
@@ -96,7 +96,7 @@ m: int | |
|
|
||
| match m: | ||
| case b.b: | ||
| reveal_type(m) | ||
| reveal_type(m) # N: Revealed type is "builtins.int" | ||
| [file b.py] | ||
| b: str | ||
| [builtins fixtures/primitives.pyi] | ||
|
|
@@ -1742,14 +1742,15 @@ from typing import NoReturn | |
| def assert_never(x: NoReturn) -> None: ... | ||
|
|
||
| class Medal(Enum): | ||
| gold = 1 | ||
| GOLD = 1 | ||
|
|
||
| def f(m: Medal) -> None: | ||
| always_assigned: int | None = None | ||
| match m: | ||
| case Medal.gold: | ||
| case Medal.GOLD: | ||
| always_assigned = 1 | ||
| reveal_type(m) # N: Revealed type is "Literal[__main__.Medal.gold]" | ||
| # This should narrow to literal, see TODO in checker::refine_identity_comparison_expression | ||
| reveal_type(m) # N: Revealed type is "__main__.Medal" | ||
| case _: | ||
| assert_never(m) | ||
|
|
||
|
|
@@ -1785,6 +1786,34 @@ def g(m: Medal) -> int: | |
| return 2 | ||
| [builtins fixtures/enum.pyi] | ||
|
|
||
| [case testMatchLiteralOrValuePattern] | ||
| # flags: --warn-unreachable | ||
| from typing import Literal | ||
|
|
||
| def test1(x: Literal[1,2,3]) -> None: | ||
| match x: | ||
| case 1: | ||
| reveal_type(x) # N: Revealed type is "Literal[1]" | ||
| case other: | ||
| reveal_type(x) # N: Revealed type is "Union[Literal[2], Literal[3]]" | ||
|
|
||
| def test2(x: Literal[1,2,3]) -> None: | ||
| match x: | ||
| case 1: | ||
| reveal_type(x) # N: Revealed type is "Literal[1]" | ||
| case 2: | ||
| reveal_type(x) # N: Revealed type is "Literal[2]" | ||
| case 3: | ||
| reveal_type(x) # N: Revealed type is "Literal[3]" | ||
| case other: | ||
| 1 # E: Statement is unreachable | ||
|
|
||
| def test3(x: Literal[1,2,3]) -> None: | ||
| match x: | ||
| case 1 | 3: | ||
| reveal_type(x) # N: Revealed type is "Union[Literal[1], Literal[3]]" | ||
| case other: | ||
| reveal_type(x) # N: Revealed type is "Literal[2]" | ||
|
|
||
| [case testMatchLiteralPatternEnumWithTypedAttribute] | ||
| from enum import Enum | ||
|
|
@@ -2813,7 +2842,7 @@ match A().foo: | |
| def int_literal() -> None: | ||
| match 12: | ||
| case 1 as s: | ||
| reveal_type(s) # N: Revealed type is "Literal[1]" | ||
| reveal_type(s) # E: Statement is unreachable | ||
| case int(i): | ||
| reveal_type(i) # N: Revealed type is "Literal[12]?" | ||
| case other: | ||
|
|
@@ -2822,7 +2851,7 @@ def int_literal() -> None: | |
| def str_literal() -> None: | ||
| match 'foo': | ||
| case 'a' as s: | ||
| reveal_type(s) # N: Revealed type is "Literal['a']" | ||
| reveal_type(s) # E: Statement is unreachable | ||
| case str(i): | ||
| reveal_type(i) # N: Revealed type is "Literal['foo']?" | ||
| case other: | ||
|
|
@@ -2909,9 +2938,9 @@ T_Choice = TypeVar("T_Choice", bound=b.One | b.Two) | |
| def switch(choice: type[T_Choice]) -> None: | ||
| match choice: | ||
| case b.One: | ||
| reveal_type(choice) # N: Revealed type is "def () -> b.One" | ||
| reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" | ||
|
Member
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. Does this narrow the type variable upper bound? (You can check e.g. by adding an attribute to only one class and accessing it here). If not, I would say this is a regression.
Collaborator
Author
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. It does not, but I won't really call that a regression - we do not support the equivalent equality case either. so either it is unsafe or unsupported, but either way does not belong to this PR. I'll open a ticket and try to look into this later. ("unsupported left operand type" is really weird to see here, but it sheds some light on why the match version does not work either)
Collaborator
Author
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. It only worked before by pure coincidence,
Member
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 understand that, but we need to minimize side effects of the fix.
Member
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. @hauntsaninja IIRC you added this test case. Does this change look OK in context of #19159
Contributor
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. Isn't the behavior that #19159 wants unsafe to begin with? In the original example, if one makes
Member
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. Yeah, I vaguely remember someone already mentioned we should allow this only if the classes are final. Anyway, I would wait to see if @hauntsaninja has any objections here.
Collaborator
Author
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. Thanks @randolf-scholz! It certainly is unsafe, a subclass will not compare equal to a superclass. Could you cross-post this observation to #19159 too, please? Negative narrowing could only work with two @ilevkivskyi positive narrowing you're asking about would also be unsafe if we pass a [case testSomething]
from typing import TypeVar
import b
T_Choice = TypeVar("T_Choice", bound=b.One | b.Two)
def switch(choice: type[T_Choice]) -> None:
match choice:
case b.One:
choice.x # AttributeError at runtime
class Mcs(type):
def __eq__(self, other): return True
class BadTwo(b.Two, metaclass=Mcs): ...
switch(BadTwo)
[file b.py]
class One:
x = 0
class Two: ...and I don't remember how custom mcs
Member
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 we can assume people don't do this :-) There are other places where user-defined
Collaborator
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. Yes, I think the change in this PR is good I don't think losing the accidental positive narrowing here will regress much (but could be good feature to add positive narrowing for this in the future — we can ignore metaclass (and yeah, points on negative narrowing are correct, I think we can just close #19159 ) |
||
| case b.Two: | ||
| reveal_type(choice) # N: Revealed type is "def () -> b.Two" | ||
| reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" | ||
| case _: | ||
| reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.