mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:37 +00:00
[ty] Infer the correct type of Enum __eq__
and __ne__
comparisions (#19666)
## Summary Resolves https://github.com/astral-sh/ty/issues/920 ## Test Plan Update `enums.md` --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
3314cf90ed
commit
24f6d2dc13
2 changed files with 110 additions and 39 deletions
|
@ -749,6 +749,51 @@ def singleton_check(value: Singleton) -> str:
|
||||||
assert_never(value)
|
assert_never(value)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `__eq__` and `__ne__`
|
||||||
|
|
||||||
|
### No `__eq__` or `__ne__` overrides
|
||||||
|
|
||||||
|
```py
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Color(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
|
||||||
|
reveal_type(Color.RED == Color.RED) # revealed: Literal[True]
|
||||||
|
reveal_type(Color.RED != Color.RED) # revealed: Literal[False]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overridden `__eq__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Color(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
reveal_type(Color.RED == Color.RED) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overridden `__ne__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Color(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
|
||||||
|
def __ne__(self, other: object) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
reveal_type(Color.RED != Color.RED) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
|
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
|
||||||
|
|
|
@ -8019,6 +8019,48 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
// language spec.
|
// language spec.
|
||||||
// - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal
|
// - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal
|
||||||
// - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal
|
// - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal
|
||||||
|
let db = self.db();
|
||||||
|
let try_dunder = |inference: &mut TypeInferenceBuilder<'db, '_>,
|
||||||
|
policy: MemberLookupPolicy| {
|
||||||
|
let rich_comparison = |op| inference.infer_rich_comparison(left, right, op, policy);
|
||||||
|
let membership_test_comparison = |op, range: TextRange| {
|
||||||
|
inference.infer_membership_test_comparison(left, right, op, range)
|
||||||
|
};
|
||||||
|
|
||||||
|
match op {
|
||||||
|
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
|
||||||
|
ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne),
|
||||||
|
ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt),
|
||||||
|
ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le),
|
||||||
|
ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt),
|
||||||
|
ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge),
|
||||||
|
ast::CmpOp::In => {
|
||||||
|
membership_test_comparison(MembershipTestCompareOperator::In, range)
|
||||||
|
}
|
||||||
|
ast::CmpOp::NotIn => {
|
||||||
|
membership_test_comparison(MembershipTestCompareOperator::NotIn, range)
|
||||||
|
}
|
||||||
|
ast::CmpOp::Is => {
|
||||||
|
if left.is_disjoint_from(db, right) {
|
||||||
|
Ok(Type::BooleanLiteral(false))
|
||||||
|
} else if left.is_singleton(db) && left.is_equivalent_to(db, right) {
|
||||||
|
Ok(Type::BooleanLiteral(true))
|
||||||
|
} else {
|
||||||
|
Ok(KnownClass::Bool.to_instance(db))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ast::CmpOp::IsNot => {
|
||||||
|
if left.is_disjoint_from(db, right) {
|
||||||
|
Ok(Type::BooleanLiteral(true))
|
||||||
|
} else if left.is_singleton(db) && left.is_equivalent_to(db, right) {
|
||||||
|
Ok(Type::BooleanLiteral(false))
|
||||||
|
} else {
|
||||||
|
Ok(KnownClass::Bool.to_instance(db))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let comparison_result = match (left, right) {
|
let comparison_result = match (left, right) {
|
||||||
(Type::Union(union), other) => {
|
(Type::Union(union), other) => {
|
||||||
let mut builder = UnionBuilder::new(self.db());
|
let mut builder = UnionBuilder::new(self.db());
|
||||||
|
@ -8233,12 +8275,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
(Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2))
|
(Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2))
|
||||||
if op == ast::CmpOp::Eq =>
|
if op == ast::CmpOp::Eq =>
|
||||||
{
|
{
|
||||||
Some(Ok(Type::BooleanLiteral(literal_1 == literal_2)))
|
Some(Ok(match try_dunder(self, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) {
|
||||||
|
Ok(ty) => ty,
|
||||||
|
Err(_) => Type::BooleanLiteral(literal_1 == literal_2),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
(Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2))
|
(Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2))
|
||||||
if op == ast::CmpOp::NotEq =>
|
if op == ast::CmpOp::NotEq =>
|
||||||
{
|
{
|
||||||
Some(Ok(Type::BooleanLiteral(literal_1 != literal_2)))
|
Some(Ok(match try_dunder(self, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) {
|
||||||
|
Ok(ty) => ty,
|
||||||
|
Err(_) => Type::BooleanLiteral(literal_1 != literal_2),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -8320,39 +8368,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final generalized fallback: lookup the rich comparison `__dunder__` methods
|
// Final generalized fallback: lookup the rich comparison `__dunder__` methods
|
||||||
let rich_comparison = |op| self.infer_rich_comparison(left, right, op);
|
try_dunder(self, MemberLookupPolicy::default())
|
||||||
let membership_test_comparison =
|
|
||||||
|op, range: TextRange| self.infer_membership_test_comparison(left, right, op, range);
|
|
||||||
match op {
|
|
||||||
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
|
|
||||||
ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne),
|
|
||||||
ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt),
|
|
||||||
ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le),
|
|
||||||
ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt),
|
|
||||||
ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge),
|
|
||||||
ast::CmpOp::In => membership_test_comparison(MembershipTestCompareOperator::In, range),
|
|
||||||
ast::CmpOp::NotIn => {
|
|
||||||
membership_test_comparison(MembershipTestCompareOperator::NotIn, range)
|
|
||||||
}
|
|
||||||
ast::CmpOp::Is => {
|
|
||||||
if left.is_disjoint_from(self.db(), right) {
|
|
||||||
Ok(Type::BooleanLiteral(false))
|
|
||||||
} else if left.is_singleton(self.db()) && left.is_equivalent_to(self.db(), right) {
|
|
||||||
Ok(Type::BooleanLiteral(true))
|
|
||||||
} else {
|
|
||||||
Ok(KnownClass::Bool.to_instance(self.db()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ast::CmpOp::IsNot => {
|
|
||||||
if left.is_disjoint_from(self.db(), right) {
|
|
||||||
Ok(Type::BooleanLiteral(true))
|
|
||||||
} else if left.is_singleton(self.db()) && left.is_equivalent_to(self.db(), right) {
|
|
||||||
Ok(Type::BooleanLiteral(false))
|
|
||||||
} else {
|
|
||||||
Ok(KnownClass::Bool.to_instance(self.db()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their
|
/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their
|
||||||
|
@ -8364,12 +8380,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
left: Type<'db>,
|
left: Type<'db>,
|
||||||
right: Type<'db>,
|
right: Type<'db>,
|
||||||
op: RichCompareOperator,
|
op: RichCompareOperator,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||||
let db = self.db();
|
let db = self.db();
|
||||||
// The following resource has details about the rich comparison algorithm:
|
// The following resource has details about the rich comparison algorithm:
|
||||||
// https://snarky.ca/unravelling-rich-comparison-operators/
|
// https://snarky.ca/unravelling-rich-comparison-operators/
|
||||||
let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| {
|
let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| {
|
||||||
left.try_call_dunder(db, op.dunder(), CallArguments::positional([right]))
|
left.try_call_dunder_with_policy(
|
||||||
|
db,
|
||||||
|
op.dunder(),
|
||||||
|
&mut CallArguments::positional([right]),
|
||||||
|
policy,
|
||||||
|
)
|
||||||
.map(|outcome| outcome.return_type(db))
|
.map(|outcome| outcome.return_type(db))
|
||||||
.ok()
|
.ok()
|
||||||
};
|
};
|
||||||
|
@ -8384,7 +8406,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
// When no appropriate method returns any value other than NotImplemented,
|
// When no appropriate method returns any value other than NotImplemented,
|
||||||
// the `==` and `!=` operators will fall back to `is` and `is not`, respectively.
|
// the `==` and `!=` operators will fall back to `is` and `is not`, respectively.
|
||||||
// refer to `<https://docs.python.org/3/reference/datamodel.html#object.__eq__>`
|
// refer to `<https://docs.python.org/3/reference/datamodel.html#object.__eq__>`
|
||||||
if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) {
|
if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne)
|
||||||
|
// This branch implements specific behavior of the `__eq__` and `__ne__` methods
|
||||||
|
// on `object`, so it does not apply if we skip looking up attributes on `object`.
|
||||||
|
&& !policy.mro_no_object_fallback()
|
||||||
|
{
|
||||||
Some(KnownClass::Bool.to_instance(db))
|
Some(KnownClass::Bool.to_instance(db))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue