[ty] Narrow equality across IntEnum classes#26079
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134. |
Memory usage reportMemory usage unchanged ✅ |
|
271078e to
1f3a791
Compare
1f3a791 to
ed905b3
Compare
5c45ea2 to
066d24c
Compare
a4005c2 to
9ac4bc2
Compare
|
\cc @AlexWaygood RE: #25788 (comment) |
9ac4bc2 to
ff03b22
Compare
|
Nice! Looks like there are still some issues with inequality narrowing on this branch: from enum import Enum, IntEnum, StrEnum
class A(int, Enum):
X = 1
Y = 2
class B(int, Enum):
X = 1
Y = 2
class C(IntEnum):
X = 1
Y = 2
class D(StrEnum):
X = "X"
Y = "Y"
class E(str, Enum):
X = "X"
Y = "Y"
def eq(a: A | B | C | D | E):
if a == 1:
reveal_type(a) # ✅ Literal[A.X, B.X, C.X]
else:
reveal_type(a) # ❌ A | B | C | D
if a == C.X:
reveal_type(a) # ✅ Literal[A.X, B.X, C.X]
else:
reveal_type(a) # Literal[A.Y, B.Y, C.Y] | D | E
if a == "X":
reveal_type(a) # ✅ Literal[D.X, E.X]
else:
reveal_type(a) # ❌ A | B | C | D
if a == D.X:
reveal_type(a) # ✅ Literal[D.X, E.X]
else:
reveal_type(a) # ✅ A | B | C | Literal[D.Y, E.Y]
def ne(a: A | B | C | D | E):
if a != 1:
reveal_type(a) # ❌ A | B | C | D
else:
reveal_type(a) # ✅ Literal[A.X, B.X, C.X]
if a != C.X:
reveal_type(a) # ✅ Literal[A.Y, B.Y, C.Y] | D | E
else:
reveal_type(a) # ✅ Literal[A.X, B.X, C.X]
if a != "X":
reveal_type(a) # ❌ A | B | C | D
else:
reveal_type(a) # ✅ Literal[D.X, E.X]
if a != D.X:
reveal_type(a) # ✅ A | B | C | Literal[D.Y, E.Y]
else:
reveal_type(a) # ✅ Literal[D.X, E.X] |
| /// Unlike `.value` inference, this trusts standard-library constructor methods whose | ||
| /// comparison behavior is modeled. User-defined or opaque constructors and custom metaclasses | ||
| /// may change that behavior arbitrarily. |
There was a problem hiding this comment.
hmmm... I'm quite surprised that we don't already reveal Literal[1] for this, to be honest:
from enum import IntEnum
class Foo(IntEnum):
X = 1
reveal_type(Foo.X.value)should we improve our .value inference elsewhere rather than reimplementing a better version of it here?
There was a problem hiding this comment.
Why is that? IntEnum defines its own __new__. We can't trust custom __new__, in general.
There was a problem hiding this comment.
Yes, but IntEnum is (as you say in the PR description, and this PR's mdtest prose) a commonly used stdlib class that seems worth adding special casing for. It does have a custom __new__, but for this particular custom __new__ we know that all it does internally is call int() on the object passed in. And we know that int(1) creates an object of type Literal[1].
There was a problem hiding this comment.
Isn't this fixing that exact behavior? That's why we track user-defined vs. standard library __new__, and feed that through to comparison_value_type.
There was a problem hiding this comment.
I see, you're suggesting that this should just be the type of .value rather than a separate function? That seems reasonable. I think I was mostly confused by "I'm quite surprised that we don't already reveal Literal[1] for this, to be honest" followed by an exact explanation of my own explanation from the summary.
There was a problem hiding this comment.
"I'm quite surprised that we don't already reveal Literal[1] for this, to be honest" suggested to me that you meant "on main", FWIW.
There was a problem hiding this comment.
I did mean "on main"! I meant I'm surprised that we don't already special-case IntEnum on main! And that rather than applying narrow special-casing for IntEnum here, we should apply it more broadly -- that it should just be the type of .value rather than a separate function
There was a problem hiding this comment.
I could probably break it into a separate PR.
ff03b22 to
7acdad9
Compare
9d0d437 to
17b768a
Compare
7acdad9 to
e624129
Compare
e624129 to
1932e15
Compare
Summary
IntEnummembers from different classes compare using their integer values, but we previously treated those members as unequal because they belonged to different enum classes:Using the precise member values now modeled for standard-library enums, this narrows comparisons across integer- and string-valued enum classes, including
==,!=, andmatchpatterns. Negative comparisons against builtin literals also exclude enum members known to compare equal to that literal; for example,value != 1removes every finite enum member whose value is1.