[ty] Move constraint set mdtest functions into `ConstraintSet` class (#21108)

We have several functions in `ty_extensions` for testing our constraint
set implementation. This PR refactors those functions so that they are
all methods of the `ConstraintSet` class, rather than being standalone
top-level functions. 🎩 to @sharkdp for pointing out that
`KnownBoundMethod` gives us what we need to implement that!
This commit is contained in:
Douglas Creager 2025-10-28 14:32:41 -04:00 committed by GitHub
parent 7b959ef44b
commit 4d2ee41e24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 407 additions and 297 deletions

View File

@ -34,7 +34,7 @@ upper bound.
```py
from typing import Any, final, Never, Sequence
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -45,7 +45,7 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Super))
```
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
@ -54,7 +54,7 @@ bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
reveal_type(range_constraint(Never, T, Base))
reveal_type(ConstraintSet.range(Never, T, Base))
```
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
@ -63,7 +63,7 @@ no upper bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
reveal_type(range_constraint(Base, T, object))
reveal_type(ConstraintSet.range(Base, T, object))
```
And a range constraint with _both_ a lower bound of `Never` and an upper bound of `object` does not
@ -72,7 +72,7 @@ constrain the typevar at all.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(range_constraint(Never, T, object))
reveal_type(ConstraintSet.range(Never, T, object))
```
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
@ -81,9 +81,9 @@ or incomparable, then there is no type that can satisfy the constraint.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(Super, T, Sub))
reveal_type(ConstraintSet.range(Super, T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(Base, T, Unrelated))
reveal_type(ConstraintSet.range(Base, T, Unrelated))
```
The lower and upper bound can be the same type, in which case the typevar can only be specialized to
@ -92,7 +92,7 @@ that specific type.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
reveal_type(range_constraint(Base, T, Base))
reveal_type(ConstraintSet.range(Base, T, Base))
```
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
@ -101,14 +101,14 @@ their bottom and top materializations, respectively.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
reveal_type(range_constraint(Base, T, Any))
reveal_type(ConstraintSet.range(Base, T, Any))
# revealed: ty_extensions.ConstraintSet[(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
reveal_type(range_constraint(Sequence[Base], T, Sequence[Any]))
reveal_type(ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
reveal_type(range_constraint(Any, T, Base))
reveal_type(ConstraintSet.range(Any, T, Base))
# revealed: ty_extensions.ConstraintSet[(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
reveal_type(range_constraint(Sequence[Any], T, Sequence[Base]))
reveal_type(ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
```
### Negated range
@ -119,7 +119,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc
```py
from typing import Any, final, Never, Sequence
from ty_extensions import negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -130,7 +130,7 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Super))
```
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
@ -139,7 +139,7 @@ bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(negated_range_constraint(Never, T, Base))
reveal_type(~ConstraintSet.range(Never, T, Base))
```
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
@ -148,7 +148,7 @@ no upper bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
reveal_type(negated_range_constraint(Base, T, object))
reveal_type(~ConstraintSet.range(Base, T, object))
```
And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object`
@ -157,7 +157,7 @@ cannot be satisfied at all.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(negated_range_constraint(Never, T, object))
reveal_type(~ConstraintSet.range(Never, T, object))
```
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
@ -166,9 +166,9 @@ or incomparable, then the negated range constraint can always be satisfied.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(Super, T, Sub))
reveal_type(~ConstraintSet.range(Super, T, Sub))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(Base, T, Unrelated))
reveal_type(~ConstraintSet.range(Base, T, Unrelated))
```
The lower and upper bound can be the same type, in which case the typevar can be specialized to any
@ -177,7 +177,7 @@ type other than that specific type.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
reveal_type(negated_range_constraint(Base, T, Base))
reveal_type(~ConstraintSet.range(Base, T, Base))
```
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
@ -186,14 +186,14 @@ their bottom and top materializations, respectively.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
reveal_type(negated_range_constraint(Base, T, Any))
reveal_type(~ConstraintSet.range(Base, T, Any))
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
reveal_type(negated_range_constraint(Sequence[Base], T, Sequence[Any]))
reveal_type(~ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(negated_range_constraint(Any, T, Base))
reveal_type(~ConstraintSet.range(Any, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
reveal_type(negated_range_constraint(Sequence[Any], T, Sequence[Base]))
reveal_type(~ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
```
## Intersection
@ -204,7 +204,7 @@ cases, we can simplify the result of an intersection.
### Different typevars
```py
from ty_extensions import range_constraint, negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -216,9 +216,9 @@ We cannot simplify the intersection of constraints that refer to different typev
```py
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[((Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base))]
reveal_type(range_constraint(Sub, T, Base) & range_constraint(Sub, U, Base))
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base))
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base))]
reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Sub, U, Base))
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base))
```
### Intersection of two ranges
@ -227,7 +227,7 @@ The intersection of two ranges is where the ranges "overlap".
```py
from typing import final
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -239,13 +239,13 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(SubSub, T, Base) & range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(SubSub, T, Super) & range_constraint(Sub, T, Base))
reveal_type(ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
reveal_type(range_constraint(Sub, T, Base) & range_constraint(Base, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(range_constraint(Sub, T, Super) & range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super))
```
If they don't overlap, the intersection is empty.
@ -253,9 +253,9 @@ If they don't overlap, the intersection is empty.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(SubSub, T, Sub) & range_constraint(Base, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(SubSub, T, Sub) & range_constraint(Unrelated, T, object))
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
```
### Intersection of a range and a negated range
@ -266,7 +266,7 @@ the intersection as removing the hole from the range constraint.
```py
from typing import final, Never
from ty_extensions import range_constraint, negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -282,9 +282,9 @@ If the negative range completely contains the positive range, then the intersect
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(SubSub, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(Sub, T, Base))
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base))
```
If the negative range is disjoint from the positive range, the negative range doesn't remove
@ -293,11 +293,11 @@ anything; the intersection is the positive range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(Never, T, Unrelated))
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub)]
reveal_type(range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super)]
reveal_type(range_constraint(Base, T, Super) & negated_range_constraint(SubSub, T, Sub))
reveal_type(ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub))
```
Otherwise we clip the negative constraint to the mininum range that overlaps with the positive
@ -306,9 +306,9 @@ range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base))
reveal_type(ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
```
### Intersection of two negated ranges
@ -318,7 +318,7 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t
```py
from typing import final
from ty_extensions import negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -330,9 +330,9 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base))
reveal_type(~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(Sub, T, Super) & negated_range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super))
```
Otherwise, the union cannot be simplified.
@ -340,11 +340,11 @@ Otherwise, the union cannot be simplified.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Base, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))]
reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))]
reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Unrelated, T, object))
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object))
```
In particular, the following does not simplify, even though it seems like it could simplify to
@ -361,7 +361,7 @@ that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the un
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))]
reveal_type(negated_range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
```
## Union
@ -372,7 +372,7 @@ can simplify the result of an union.
### Different typevars
```py
from ty_extensions import range_constraint, negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -384,9 +384,9 @@ We cannot simplify the union of constraints that refer to different typevars.
```py
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)]
reveal_type(range_constraint(Sub, T, Base) | range_constraint(Sub, U, Base))
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base) ¬(Sub ≤ U@_ ≤ Base)]
reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Sub, U, Base))
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base))
```
### Union of two ranges
@ -396,7 +396,7 @@ bounds.
```py
from typing import final
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -408,9 +408,9 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Super)]
reveal_type(range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base))
reveal_type(ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(range_constraint(Sub, T, Super) | range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super))
```
Otherwise, the union cannot be simplified.
@ -418,11 +418,11 @@ Otherwise, the union cannot be simplified.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(Sub, T, Base) | range_constraint(Base, T, Super))
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Sub)]
reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) (Unrelated ≤ T@_)]
reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Unrelated, T, object))
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object))
```
In particular, the following does not simplify, even though it seems like it could simplify to
@ -438,7 +438,7 @@ not include `Sub`. That means it should not be in the union. Since that type _is
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super))
reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
```
### Union of a range and a negated range
@ -449,7 +449,7 @@ the union as filling part of the hole with the types from the range constraint.
```py
from typing import final, Never
from ty_extensions import range_constraint, negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -465,9 +465,9 @@ If the positive range completely contains the negative range, then the union is
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(SubSub, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(Sub, T, Base))
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base))
```
If the negative range is disjoint from the positive range, the positive range doesn't add anything;
@ -476,11 +476,11 @@ the union is the negative range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(Never, T, Unrelated))
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated))
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Sub)]
reveal_type(negated_range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(Base, T, Super) | range_constraint(SubSub, T, Sub))
reveal_type(~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub))
```
Otherwise we clip the positive constraint to the mininum range that overlaps with the negative
@ -489,9 +489,9 @@ range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base))
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
```
### Union of two negated ranges
@ -500,7 +500,7 @@ The union of two negated ranges has a hole where the ranges "overlap".
```py
from typing import final
from ty_extensions import negated_range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -512,13 +512,13 @@ class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(SubSub, T, Base) | negated_range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(SubSub, T, Super) | negated_range_constraint(Sub, T, Base))
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Base, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(Sub, T, Super) | negated_range_constraint(Sub, T, Super))
reveal_type(~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super))
```
If the holes don't overlap, the union is always satisfied.
@ -526,9 +526,9 @@ If the holes don't overlap, the union is always satisfied.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(SubSub, T, Sub) | negated_range_constraint(Base, T, Super))
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(negated_range_constraint(SubSub, T, Sub) | negated_range_constraint(Unrelated, T, object))
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object))
```
## Negation
@ -537,7 +537,7 @@ def _[T]() -> None:
```py
from typing import Never
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
class Super: ...
class Base(Super): ...
@ -545,20 +545,20 @@ class Sub(Base): ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(~range_constraint(Sub, T, Base))
reveal_type(~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(~range_constraint(Never, T, Base))
reveal_type(~ConstraintSet.range(Never, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_)]
reveal_type(~range_constraint(Sub, T, object))
reveal_type(~ConstraintSet.range(Sub, T, object))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(~range_constraint(Never, T, object))
reveal_type(~ConstraintSet.range(Never, T, object))
```
The union of a range constraint and its negation should always be satisfiable.
```py
def _[T]() -> None:
constraint = range_constraint(Sub, T, Base)
constraint = ConstraintSet.range(Sub, T, Base)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(constraint | ~constraint)
```
@ -567,7 +567,7 @@ def _[T]() -> None:
```py
from typing import final, Never
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
class Base: ...
@ -576,20 +576,20 @@ class Unrelated: ...
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base) ¬(U@_ ≤ Base)]
reveal_type(~(range_constraint(Never, T, Base) & range_constraint(Never, U, Base)))
reveal_type(~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)))
```
The union of a constraint and its negation should always be satisfiable.
```py
def _[T, U]() -> None:
c1 = range_constraint(Never, T, Base) & range_constraint(Never, U, Base)
c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c1 | ~c1)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c1 | c1)
c2 = range_constraint(Unrelated, T, object) & range_constraint(Unrelated, U, object)
c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c2 | ~c2)
# revealed: ty_extensions.ConstraintSet[always]
@ -614,19 +614,19 @@ since we always hide `Never` lower bounds and `object` upper bounds.
```py
from typing import Never
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(range_constraint(Never, S, T))
reveal_type(ConstraintSet.range(Never, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(range_constraint(S, T, object))
reveal_type(ConstraintSet.range(S, T, object))
def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(range_constraint(Never, S, T))
reveal_type(ConstraintSet.range(Never, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(range_constraint(S, T, object))
reveal_type(ConstraintSet.range(S, T, object))
```
Equivalence constraints are similar; internally we arbitrarily choose the "earlier" typevar to be
@ -635,15 +635,15 @@ the constraint, and the other the bound. But we display the result the same way
```py
def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(range_constraint(T, S, T))
reveal_type(ConstraintSet.range(T, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(range_constraint(S, T, S))
reveal_type(ConstraintSet.range(S, T, S))
def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(range_constraint(T, S, T))
reveal_type(ConstraintSet.range(T, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(range_constraint(S, T, S))
reveal_type(ConstraintSet.range(S, T, S))
```
But in the case of `S ≤ T ≤ U`, we end up with an ambiguity. Depending on the typevar ordering, that
@ -654,7 +654,7 @@ def f[S, T, U]():
# Could be either of:
# ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)]
# ty_extensions.ConstraintSet[(S@f ≤ T@f) ∧ (T@f ≤ U@f)]
# reveal_type(range_constraint(S, T, U))
# reveal_type(ConstraintSet.range(S, T, U))
...
```
@ -668,13 +668,13 @@ This section contains several examples that show that we simplify the DNF formul
before displaying it.
```py
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
def f[T, U]():
t1 = range_constraint(str, T, str)
t2 = range_constraint(bool, T, bool)
u1 = range_constraint(str, U, str)
u2 = range_constraint(bool, U, bool)
t1 = ConstraintSet.range(str, T, str)
t2 = ConstraintSet.range(bool, T, bool)
u1 = ConstraintSet.range(str, U, str)
u2 = ConstraintSet.range(bool, U, bool)
# revealed: ty_extensions.ConstraintSet[(T@f = bool) (T@f = str)]
reveal_type(t1 | t2)
@ -692,8 +692,8 @@ from typing import Never
from ty_extensions import static_assert
def f[T]():
t_int = range_constraint(Never, T, int)
t_bool = range_constraint(Never, T, bool)
t_int = ConstraintSet.range(Never, T, int)
t_bool = ConstraintSet.range(Never, T, bool)
# `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the
# latter. We can turn that into a constraint set, using the equivalence `p → q == ¬p q`:
@ -707,7 +707,7 @@ def f[T]():
# "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two
# constraint sets that are both always satisfied will not be identical if they have different
# domains!
always = range_constraint(Never, T, object)
always = ConstraintSet.range(Never, T, object)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(always)
static_assert(always)
@ -721,11 +721,11 @@ intersections whose elements appear in different orders.
```py
from typing import Never
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
def f[T]():
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(range_constraint(Never, T, str | int))
reveal_type(ConstraintSet.range(Never, T, str | int))
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(range_constraint(Never, T, int | str))
reveal_type(ConstraintSet.range(Never, T, int | str))
```

View File

@ -5,7 +5,7 @@
python-version = "3.12"
```
This file tests the _constraint implication_ relationship between types, aka `is_subtype_of_given`,
This file tests the _constraint implication_ relationship between types, aka `implies_subtype_of`,
which tests whether one type is a [subtype][subtyping] of another _assuming that the constraints in
a particular constraint set hold_.
@ -16,14 +16,14 @@ fully static type that is not a typevar. It can _contain_ a typevar, though —
considered concrete.)
```py
from ty_extensions import is_subtype_of, is_subtype_of_given, static_assert
from ty_extensions import ConstraintSet, is_subtype_of, static_assert
def equivalent_to_other_relationships[T]():
static_assert(is_subtype_of(bool, int))
static_assert(is_subtype_of_given(True, bool, int))
static_assert(ConstraintSet.always().implies_subtype_of(bool, int))
static_assert(not is_subtype_of(bool, str))
static_assert(not is_subtype_of_given(True, bool, str))
static_assert(not ConstraintSet.always().implies_subtype_of(bool, str))
```
Moreover, for concrete types, the answer does not depend on which constraint set we are considering.
@ -32,16 +32,16 @@ there isn't a valid specialization for the typevars we are considering.
```py
from typing import Never
from ty_extensions import range_constraint
from ty_extensions import ConstraintSet
def even_given_constraints[T]():
constraints = range_constraint(Never, T, int)
static_assert(is_subtype_of_given(constraints, bool, int))
static_assert(not is_subtype_of_given(constraints, bool, str))
constraints = ConstraintSet.range(Never, T, int)
static_assert(constraints.implies_subtype_of(bool, int))
static_assert(not constraints.implies_subtype_of(bool, str))
def even_given_unsatisfiable_constraints():
static_assert(is_subtype_of_given(False, bool, int))
static_assert(not is_subtype_of_given(False, bool, str))
static_assert(ConstraintSet.never().implies_subtype_of(bool, int))
static_assert(not ConstraintSet.never().implies_subtype_of(bool, str))
```
## Type variables
@ -141,37 +141,37 @@ considering.
```py
from typing import Never
from ty_extensions import is_subtype_of_given, range_constraint, static_assert
from ty_extensions import ConstraintSet, static_assert
def given_constraints[T]():
static_assert(not is_subtype_of_given(True, T, int))
static_assert(not is_subtype_of_given(True, T, bool))
static_assert(not is_subtype_of_given(True, T, str))
static_assert(not ConstraintSet.always().implies_subtype_of(T, int))
static_assert(not ConstraintSet.always().implies_subtype_of(T, bool))
static_assert(not ConstraintSet.always().implies_subtype_of(T, str))
# These are vacuously true; false implies anything
static_assert(is_subtype_of_given(False, T, int))
static_assert(is_subtype_of_given(False, T, bool))
static_assert(is_subtype_of_given(False, T, str))
static_assert(ConstraintSet.never().implies_subtype_of(T, int))
static_assert(ConstraintSet.never().implies_subtype_of(T, bool))
static_assert(ConstraintSet.never().implies_subtype_of(T, str))
given_int = range_constraint(Never, T, int)
static_assert(is_subtype_of_given(given_int, T, int))
static_assert(not is_subtype_of_given(given_int, T, bool))
static_assert(not is_subtype_of_given(given_int, T, str))
given_int = ConstraintSet.range(Never, T, int)
static_assert(given_int.implies_subtype_of(T, int))
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
given_bool = range_constraint(Never, T, bool)
static_assert(is_subtype_of_given(given_bool, T, int))
static_assert(is_subtype_of_given(given_bool, T, bool))
static_assert(not is_subtype_of_given(given_bool, T, str))
given_bool = ConstraintSet.range(Never, T, bool)
static_assert(given_bool.implies_subtype_of(T, int))
static_assert(given_bool.implies_subtype_of(T, bool))
static_assert(not given_bool.implies_subtype_of(T, str))
given_both = given_bool & given_int
static_assert(is_subtype_of_given(given_both, T, int))
static_assert(is_subtype_of_given(given_both, T, bool))
static_assert(not is_subtype_of_given(given_both, T, str))
static_assert(given_both.implies_subtype_of(T, int))
static_assert(given_both.implies_subtype_of(T, bool))
static_assert(not given_both.implies_subtype_of(T, str))
given_str = range_constraint(Never, T, str)
static_assert(not is_subtype_of_given(given_str, T, int))
static_assert(not is_subtype_of_given(given_str, T, bool))
static_assert(is_subtype_of_given(given_str, T, str))
given_str = ConstraintSet.range(Never, T, str)
static_assert(not given_str.implies_subtype_of(T, int))
static_assert(not given_str.implies_subtype_of(T, bool))
static_assert(given_str.implies_subtype_of(T, str))
```
This might require propagating constraints from other typevars.
@ -179,20 +179,20 @@ This might require propagating constraints from other typevars.
```py
def mutually_constrained[T, U]():
# If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well.
given_int = range_constraint(U, T, U) & range_constraint(Never, U, int)
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
# TODO: no static-assert-error
# error: [static-assert-error]
static_assert(is_subtype_of_given(given_int, T, int))
static_assert(not is_subtype_of_given(given_int, T, bool))
static_assert(not is_subtype_of_given(given_int, T, str))
static_assert(given_int.implies_subtype_of(T, int))
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
# If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well.
given_int = range_constraint(Never, T, U) & range_constraint(Never, U, int)
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
# TODO: no static-assert-error
# error: [static-assert-error]
static_assert(is_subtype_of_given(given_int, T, int))
static_assert(not is_subtype_of_given(given_int, T, bool))
static_assert(not is_subtype_of_given(given_int, T, str))
static_assert(given_int.implies_subtype_of(T, int))
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
```
[subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence

View File

@ -4119,6 +4119,39 @@ impl<'db> Type<'db> {
Place::bound(Type::KnownBoundMethod(KnownBoundMethodType::PathOpen)).into()
}
Type::ClassLiteral(class)
if name == "range" && class.is_known(db, KnownClass::ConstraintSet) =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetRange,
))
.into()
}
Type::ClassLiteral(class)
if name == "always" && class.is_known(db, KnownClass::ConstraintSet) =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetAlways,
))
.into()
}
Type::ClassLiteral(class)
if name == "never" && class.is_known(db, KnownClass::ConstraintSet) =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetNever,
))
.into()
}
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
if name == "implies_subtype_of" =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(tracked),
))
.into()
}
Type::ClassLiteral(class)
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
{
@ -4833,51 +4866,6 @@ impl<'db> Type<'db> {
)
.into(),
Some(KnownFunction::IsSubtypeOfGiven) => Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("constraints")))
.with_annotated_type(UnionType::from_elements(
db,
[
KnownClass::Bool.to_instance(db),
KnownClass::ConstraintSet.to_instance(db),
],
)),
Parameter::positional_only(Some(Name::new_static("ty")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("of")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::ConstraintSet.to_instance(db)),
),
)
.into(),
Some(KnownFunction::RangeConstraint | KnownFunction::NegatedRangeConstraint) => {
Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("lower_bound")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("typevar")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("upper_bound")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::ConstraintSet.to_instance(db)),
),
)
.into()
}
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single(
self,
@ -6918,7 +6906,14 @@ impl<'db> Type<'db> {
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
// A non-generic class never needs to be specialized. A generic class is specialized
@ -7064,7 +7059,12 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen,
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@ -10318,6 +10318,12 @@ pub enum KnownBoundMethodType<'db> {
StrStartswith(StringLiteralType<'db>),
/// Method wrapper for `Path.open`,
PathOpen,
// ConstraintSet methods
ConstraintSetRange,
ConstraintSetAlways,
ConstraintSetNever,
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
}
pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@ -10341,7 +10347,11 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
KnownBoundMethodType::StrStartswith(string_literal) => {
visitor.visit_type(db, Type::StringLiteral(string_literal));
}
KnownBoundMethodType::PathOpen => {}
KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {}
}
}
@ -10393,9 +10403,23 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => {
ConstraintSet::from(true)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
| (
KnownBoundMethodType::ConstraintSetAlways,
KnownBoundMethodType::ConstraintSetAlways,
)
| (
KnownBoundMethodType::ConstraintSetNever,
KnownBoundMethodType::ConstraintSetNever,
)
| (
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(true),
(
KnownBoundMethodType::FunctionTypeDunderGet(_)
@ -10403,13 +10427,21 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen,
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen,
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(false),
}
}
@ -10445,9 +10477,26 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => {
ConstraintSet::from(true)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
| (
KnownBoundMethodType::ConstraintSetAlways,
KnownBoundMethodType::ConstraintSetAlways,
)
| (
KnownBoundMethodType::ConstraintSetNever,
KnownBoundMethodType::ConstraintSetNever,
) => ConstraintSet::from(true),
(
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints),
) => left_constraints
.constraints(db)
.iff(db, right_constraints.constraints(db)),
(
KnownBoundMethodType::FunctionTypeDunderGet(_)
@ -10455,13 +10504,21 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen,
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen,
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(false),
}
}
@ -10480,7 +10537,12 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PropertyDunderSet(property) => {
KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor))
}
KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen => self,
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => self,
}
}
@ -10493,6 +10555,10 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType,
KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType,
KnownBoundMethodType::PathOpen => KnownClass::MethodType,
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet,
}
}
@ -10592,6 +10658,45 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PathOpen => {
Either::Right(std::iter::once(Signature::todo("`Path.open` return type")))
}
KnownBoundMethodType::ConstraintSetRange => {
Either::Right(std::iter::once(Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("lower_bound")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("typevar")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("upper_bound")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::ConstraintSet.to_instance(db)),
)))
}
KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever => {
Either::Right(std::iter::once(Signature::new(
Parameters::empty(),
Some(KnownClass::ConstraintSet.to_instance(db)),
)))
}
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {
Either::Right(std::iter::once(Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("ty")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("of")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::ConstraintSet.to_instance(db)),
)))
}
}
}
}

View File

@ -705,33 +705,6 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::IsSubtypeOfGiven) => {
let [Some(constraints), Some(ty_a), Some(ty_b)] =
overload.parameter_types()
else {
continue;
};
let constraints = match constraints {
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) => {
tracked.constraints(db)
}
Type::BooleanLiteral(b) => ConstraintSet::from(*b),
_ => continue,
};
let result = constraints.when_subtype_of_given(
db,
*ty_a,
*ty_b,
InferableTypeVars::None,
);
let tracked = TrackedConstraintSet::new(db, result);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::ConstraintSet(tracked),
));
}
Some(KnownFunction::IsAssignableTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
let constraints =
@ -1149,6 +1122,60 @@ impl<'db> Bindings<'db> {
}
},
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => {
let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] =
overload.parameter_types()
else {
return;
};
let constraints = ConstraintSet::range(db, *lower, *typevar, *upper);
let tracked = TrackedConstraintSet::new(db, constraints);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::ConstraintSet(tracked),
));
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => {
if !overload.parameter_types().is_empty() {
return;
}
let constraints = ConstraintSet::from(true);
let tracked = TrackedConstraintSet::new(db, constraints);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::ConstraintSet(tracked),
));
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => {
if !overload.parameter_types().is_empty() {
return;
}
let constraints = ConstraintSet::from(false);
let tracked = TrackedConstraintSet::new(db, constraints);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::ConstraintSet(tracked),
));
}
Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(tracked),
) => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
continue;
};
let result = tracked.constraints(db).when_subtype_of_given(
db,
*ty_a,
*ty_b,
InferableTypeVars::None,
);
let tracked = TrackedConstraintSet::new(db, result);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::ConstraintSet(tracked),
));
}
Type::ClassLiteral(class) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),

View File

@ -295,6 +295,12 @@ impl<'db> ConstraintSet<'db> {
self
}
pub(crate) fn iff(self, db: &'db dyn Db, other: Self) -> Self {
ConstraintSet {
node: self.node.iff(db, other.node),
}
}
pub(crate) fn range(
db: &'db dyn Db,
lower: Type<'db>,
@ -304,15 +310,6 @@ impl<'db> ConstraintSet<'db> {
Self::constrain_typevar(db, typevar, lower, upper, TypeRelation::Assignability)
}
pub(crate) fn negated_range(
db: &'db dyn Db,
lower: Type<'db>,
typevar: BoundTypeVarInstance<'db>,
upper: Type<'db>,
) -> Self {
Self::range(db, lower, typevar, upper).negate(db)
}
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
self.node.simplify(db).display(db)
}

View File

@ -523,6 +523,18 @@ impl Display for DisplayRepresentation<'_> {
Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => {
f.write_str("bound method `Path.open`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => {
f.write_str("bound method `ConstraintSet.range`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => {
f.write_str("bound method `ConstraintSet.always`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => {
f.write_str("bound method `ConstraintSet.never`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => {
f.write_str("bound method `ConstraintSet.implies_subtype_of`")
}
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),

View File

@ -81,9 +81,9 @@ use crate::types::visitor::any_over_type;
use crate::types::{
ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase,
ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType,
NormalizedVisitor, SpecialFormType, TrackedConstraintSet, Truthiness, Type, TypeContext,
TypeMapping, TypeRelation, UnionBuilder, binding_type, todo_type, walk_signature,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, NormalizedVisitor,
SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, UnionBuilder,
binding_type, todo_type, walk_signature,
};
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@ -1299,8 +1299,6 @@ pub enum KnownFunction {
IsEquivalentTo,
/// `ty_extensions.is_subtype_of`
IsSubtypeOf,
/// `ty_extensions.is_subtype_of_given`
IsSubtypeOfGiven,
/// `ty_extensions.is_assignable_to`
IsAssignableTo,
/// `ty_extensions.is_disjoint_from`
@ -1323,10 +1321,6 @@ pub enum KnownFunction {
RevealProtocolInterface,
/// `ty_extensions.reveal_mro`
RevealMro,
/// `ty_extensions.range_constraint`
RangeConstraint,
/// `ty_extensions.negated_range_constraint`
NegatedRangeConstraint,
}
impl KnownFunction {
@ -1393,15 +1387,12 @@ impl KnownFunction {
| Self::IsSingleValued
| Self::IsSingleton
| Self::IsSubtypeOf
| Self::IsSubtypeOfGiven
| Self::GenericContext
| Self::DunderAllNames
| Self::EnumMembers
| Self::StaticAssert
| Self::HasMember
| Self::RevealProtocolInterface
| Self::RangeConstraint
| Self::NegatedRangeConstraint
| Self::RevealMro
| Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(),
@ -1780,32 +1771,6 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::RangeConstraint => {
let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types
else {
return;
};
let constraints = ConstraintSet::range(db, *lower, *typevar, *upper);
let tracked = TrackedConstraintSet::new(db, constraints);
overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet(
tracked,
)));
}
KnownFunction::NegatedRangeConstraint => {
let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types
else {
return;
};
let constraints = ConstraintSet::negated_range(db, *lower, *typevar, *upper);
let tracked = TrackedConstraintSet::new(db, constraints);
overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet(
tracked,
)));
}
KnownFunction::Open => {
// TODO: Temporary special-casing for `builtins.open` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
@ -1894,7 +1859,6 @@ pub(crate) mod tests {
KnownFunction::IsSingleton
| KnownFunction::IsSubtypeOf
| KnownFunction::IsSubtypeOfGiven
| KnownFunction::GenericContext
| KnownFunction::DunderAllNames
| KnownFunction::EnumMembers
@ -1905,8 +1869,6 @@ pub(crate) mod tests {
| KnownFunction::IsEquivalentTo
| KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface
| KnownFunction::RangeConstraint
| KnownFunction::NegatedRangeConstraint
| KnownFunction::RevealMro
| KnownFunction::AllMembers => KnownModule::TyExtensions,

View File

@ -44,6 +44,29 @@ type JustComplex = TypeOf[1.0j]
# Constraints
class ConstraintSet:
@staticmethod
def range(lower_bound: Any, typevar: Any, upper_bound: Any) -> Self:
"""
Returns a constraint set that requires `typevar` to specialize to a type
that is a supertype of `lower_bound` and a subtype of `upper_bound`.
"""
@staticmethod
def always() -> Self:
"""Returns a constraint set that is always satisfied"""
@staticmethod
def never() -> Self:
"""Returns a constraint set that is never satisfied"""
def implies_subtype_of(self, ty: Any, of: Any) -> Self:
"""
Returns a constraint set that is satisfied when `ty` is a `subtype`_ of
`of`, assuming that all of the constraints in `self` hold.
.. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
"""
def __bool__(self) -> bool: ...
def __eq__(self, other: ConstraintSet) -> bool: ...
def __ne__(self, other: ConstraintSet) -> bool: ...
@ -51,13 +74,6 @@ class ConstraintSet:
def __or__(self, other: ConstraintSet) -> ConstraintSet: ...
def __invert__(self) -> ConstraintSet: ...
def range_constraint(
lower_bound: Any, typevar: Any, upper_bound: Any
) -> ConstraintSet: ...
def negated_range_constraint(
lower_bound: Any, typevar: Any, upper_bound: Any
) -> ConstraintSet: ...
# Predicates on types
#
# Ideally, these would be annotated using `TypeForm`, but that has not been
@ -75,15 +91,6 @@ def is_subtype_of(ty: Any, of: Any) -> ConstraintSet:
.. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
"""
def is_subtype_of_given(
constraints: bool | ConstraintSet, ty: Any, of: Any
) -> ConstraintSet:
"""Returns a constraint set that is satisfied when `ty` is a `subtype`_ of `of`,
assuming that all of the constraints in `constraints` hold.
.. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
"""
def is_assignable_to(ty: Any, to: Any) -> ConstraintSet:
"""Returns a constraint set that is satisfied when `ty` is `assignable`_ to `to`.