mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-08 05:35:10 +00:00
Equivalence
This commit is contained in:
parent
969efae929
commit
0252ee6531
2 changed files with 116 additions and 0 deletions
|
@ -23,6 +23,98 @@ def f() -> None:
|
|||
reveal_type(x) # revealed: IntOrStr
|
||||
```
|
||||
|
||||
## Type properties
|
||||
|
||||
### Equivalence
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_equivalent_to
|
||||
|
||||
type IntOrStr = int | str
|
||||
type StrOrInt = str | int
|
||||
|
||||
static_assert(is_equivalent_to(IntOrStr, IntOrStr))
|
||||
static_assert(is_equivalent_to(IntOrStr, StrOrInt))
|
||||
|
||||
type Rec1 = tuple[Rec1, int]
|
||||
type Rec2 = tuple[Rec2, int]
|
||||
|
||||
type Other = tuple[Other, str]
|
||||
|
||||
static_assert(is_equivalent_to(Rec1, Rec2))
|
||||
static_assert(not is_equivalent_to(Rec1, Other))
|
||||
|
||||
type Cycle1A = tuple[Cycle1B, int]
|
||||
type Cycle1B = tuple[Cycle1A, str]
|
||||
|
||||
type Cycle2A = tuple[Cycle2B, int]
|
||||
type Cycle2B = tuple[Cycle2A, str]
|
||||
|
||||
static_assert(is_equivalent_to(Cycle1A, Cycle2A))
|
||||
static_assert(is_equivalent_to(Cycle1B, Cycle2B))
|
||||
static_assert(not is_equivalent_to(Cycle1A, Cycle1B))
|
||||
static_assert(not is_equivalent_to(Cycle1A, Cycle2B))
|
||||
|
||||
# type Cycle3A = tuple[Cycle3B] | None
|
||||
# type Cycle3B = tuple[Cycle3A] | None
|
||||
|
||||
# static_assert(is_equivalent_to(Cycle3A, Cycle3A))
|
||||
# static_assert(is_equivalent_to(Cycle3A, Cycle3B))
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
x1: IntOrStr = 1
|
||||
x2: IntOrStr = "1"
|
||||
x3: IntOrStr | None = None
|
||||
|
||||
def _(int_or_str: IntOrStr) -> None:
|
||||
# TODO: those should not be errors
|
||||
x3: int | str = int_or_str # error: [invalid-assignment]
|
||||
x4: int | str | None = int_or_str # error: [invalid-assignment]
|
||||
x5: int | str | None = int_or_str or None # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Narrowing (intersections)
|
||||
|
||||
```py
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
type EitherOr = P | Q
|
||||
|
||||
def _(x: EitherOr) -> None:
|
||||
if isinstance(x, P):
|
||||
reveal_type(x) # revealed: P
|
||||
elif isinstance(x, Q):
|
||||
reveal_type(x) # revealed: Q & ~P
|
||||
else:
|
||||
# TODO: This should be Never
|
||||
reveal_type(x) # revealed: EitherOr & ~P & ~Q
|
||||
```
|
||||
|
||||
### Fully static
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import static_assert, is_fully_static
|
||||
|
||||
type IntOrStr = int | str
|
||||
type RecFullyStatic = int | tuple[RecFullyStatic]
|
||||
|
||||
static_assert(is_fully_static(IntOrStr))
|
||||
static_assert(is_fully_static(RecFullyStatic))
|
||||
|
||||
type IntOrAny = int | Any
|
||||
type RecNotFullyStatic = Any | tuple[RecNotFullyStatic]
|
||||
|
||||
static_assert(not is_fully_static(IntOrAny))
|
||||
static_assert(not is_fully_static(RecNotFullyStatic))
|
||||
```
|
||||
|
||||
## `__value__` attribute
|
||||
|
||||
```py
|
||||
|
|
|
@ -275,6 +275,25 @@ fn is_fully_static_cycle_initial<'db>(_db: &'db dyn Db, _self: Type<'db>, _dummy
|
|||
true
|
||||
}
|
||||
|
||||
#[expect(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_equivalent_to_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &bool,
|
||||
_count: u32,
|
||||
_self: Type<'db>,
|
||||
_other: Type<'db>,
|
||||
) -> salsa::CycleRecoveryAction<bool> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn is_equivalent_to_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_self: Type<'db>,
|
||||
_other: Type<'db>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Meta data for `Type::Todo`, which represents a known limitation in ty.
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
@ -1706,6 +1725,8 @@ impl<'db> Type<'db> {
|
|||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
///
|
||||
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||
|
||||
#[salsa::tracked(cycle_fn=is_equivalent_to_cycle_recover, cycle_initial=is_equivalent_to_cycle_initial)]
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
// TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc.
|
||||
|
||||
|
@ -1735,6 +1756,9 @@ impl<'db> Type<'db> {
|
|||
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
|
||||
n.class.is_object(db) && protocol.normalized(db) == nominal
|
||||
}
|
||||
(Type::TypeAliasRef(left), right) => left.value_type(db).is_equivalent_to(db, right),
|
||||
(left, Type::TypeAliasRef(right)) => left.is_equivalent_to(db, right.value_type(db)),
|
||||
|
||||
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue