[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 ```py
from typing import Any, final, Never, Sequence from typing import Any, final, Never, Sequence
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -45,7 +45,7 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always] # 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) 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never] # revealed: ty_extensions.ConstraintSet[never]
reveal_type(range_constraint(Super, T, Sub)) reveal_type(ConstraintSet.range(Super, T, Sub))
# revealed: ty_extensions.ConstraintSet[never] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)] # 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])] # 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)] # 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])] # 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 ### Negated range
@ -119,7 +119,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc
```py ```py
from typing import Any, final, Never, Sequence from typing import Any, final, Never, Sequence
from ty_extensions import negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -130,7 +130,7 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)] # 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` 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never] # 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) 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always] # 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] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)] # 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])] # 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)] # 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])] # 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 ## Intersection
@ -204,7 +204,7 @@ cases, we can simplify the result of an intersection.
### Different typevars ### Different typevars
```py ```py
from ty_extensions import range_constraint, negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -216,9 +216,9 @@ We cannot simplify the intersection of constraints that refer to different typev
```py ```py
def _[T, U]() -> None: def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[((Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base))] # 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))] # 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 ### Intersection of two ranges
@ -227,7 +227,7 @@ The intersection of two ranges is where the ranges "overlap".
```py ```py
from typing import final from typing import final
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -239,13 +239,13 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)] # 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)] # 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)] # 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)] # 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. If they don't overlap, the intersection is empty.
@ -253,9 +253,9 @@ If they don't overlap, the intersection is empty.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never] # 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] # 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 ### Intersection of a range and a negated range
@ -266,7 +266,7 @@ the intersection as removing the hole from the range constraint.
```py ```py
from typing import final, Never from typing import final, Never
from ty_extensions import range_constraint, negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -282,9 +282,9 @@ If the negative range completely contains the positive range, then the intersect
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never] # 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] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)] # 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)] # 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)] # 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 Otherwise we clip the negative constraint to the mininum range that overlaps with the positive
@ -306,9 +306,9 @@ range.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))] # 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))] # 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 ### Intersection of two negated ranges
@ -318,7 +318,7 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t
```py ```py
from typing import final from typing import final
from ty_extensions import negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -330,9 +330,9 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super)] # 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)] # 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. Otherwise, the union cannot be simplified.
@ -340,11 +340,11 @@ Otherwise, the union cannot be simplified.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))] # 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))] # 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@_))] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))] # 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 ## Union
@ -372,7 +372,7 @@ can simplify the result of an union.
### Different typevars ### Different typevars
```py ```py
from ty_extensions import range_constraint, negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -384,9 +384,9 @@ We cannot simplify the union of constraints that refer to different typevars.
```py ```py
def _[T, U]() -> None: def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)] # 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)] # 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 ### Union of two ranges
@ -396,7 +396,7 @@ bounds.
```py ```py
from typing import final from typing import final
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -408,9 +408,9 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Super)] # 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)] # 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. Otherwise, the union cannot be simplified.
@ -418,11 +418,11 @@ Otherwise, the union cannot be simplified.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)] # 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)] # 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@_)] # 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 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Base)] # 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 ### 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 ```py
from typing import final, Never from typing import final, Never
from ty_extensions import range_constraint, negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -465,9 +465,9 @@ If the positive range completely contains the negative range, then the union is
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always] # 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] # 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; 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] # 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)] # 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)] # 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 Otherwise we clip the positive constraint to the mininum range that overlaps with the negative
@ -489,9 +489,9 @@ range.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Base)] # 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)] # 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 ### Union of two negated ranges
@ -500,7 +500,7 @@ The union of two negated ranges has a hole where the ranges "overlap".
```py ```py
from typing import final from typing import final
from ty_extensions import negated_range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -512,13 +512,13 @@ class Unrelated: ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] # 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)] # 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)] # 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)] # 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. 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 ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always] # 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] # 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 ## Negation
@ -537,7 +537,7 @@ def _[T]() -> None:
```py ```py
from typing import Never from typing import Never
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -545,20 +545,20 @@ class Sub(Base): ...
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] # 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)] # 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@_)] # 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] # 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. The union of a range constraint and its negation should always be satisfiable.
```py ```py
def _[T]() -> None: def _[T]() -> None:
constraint = range_constraint(Sub, T, Base) constraint = ConstraintSet.range(Sub, T, Base)
# revealed: ty_extensions.ConstraintSet[always] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(constraint | ~constraint) reveal_type(constraint | ~constraint)
``` ```
@ -567,7 +567,7 @@ def _[T]() -> None:
```py ```py
from typing import final, Never from typing import final, Never
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
class Base: ... class Base: ...
@ -576,20 +576,20 @@ class Unrelated: ...
def _[T, U]() -> None: def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base) ¬(U@_ ≤ Base)] # 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. The union of a constraint and its negation should always be satisfiable.
```py ```py
def _[T, U]() -> None: 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] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(c1 | ~c1) reveal_type(c1 | ~c1)
# revealed: ty_extensions.ConstraintSet[always] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c1 | c1) 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] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(c2 | ~c2) reveal_type(c2 | ~c2)
# revealed: ty_extensions.ConstraintSet[always] # revealed: ty_extensions.ConstraintSet[always]
@ -614,19 +614,19 @@ since we always hide `Never` lower bounds and `object` upper bounds.
```py ```py
from typing import Never from typing import Never
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
def f[S, T](): def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] # 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)] # 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](): def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] # 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)] # 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 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 ```py
def f[S, T](): def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)] # 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)] # 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](): def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)] # 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)] # 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 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: # Could be either of:
# ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)] # ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)]
# ty_extensions.ConstraintSet[(S@f ≤ T@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. before displaying it.
```py ```py
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
def f[T, U](): def f[T, U]():
t1 = range_constraint(str, T, str) t1 = ConstraintSet.range(str, T, str)
t2 = range_constraint(bool, T, bool) t2 = ConstraintSet.range(bool, T, bool)
u1 = range_constraint(str, U, str) u1 = ConstraintSet.range(str, U, str)
u2 = range_constraint(bool, U, bool) u2 = ConstraintSet.range(bool, U, bool)
# revealed: ty_extensions.ConstraintSet[(T@f = bool) (T@f = str)] # revealed: ty_extensions.ConstraintSet[(T@f = bool) (T@f = str)]
reveal_type(t1 | t2) reveal_type(t1 | t2)
@ -692,8 +692,8 @@ from typing import Never
from ty_extensions import static_assert from ty_extensions import static_assert
def f[T](): def f[T]():
t_int = range_constraint(Never, T, int) t_int = ConstraintSet.range(Never, T, int)
t_bool = range_constraint(Never, T, bool) t_bool = ConstraintSet.range(Never, T, bool)
# `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the # `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`: # 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 # "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 # constraint sets that are both always satisfied will not be identical if they have different
# domains! # domains!
always = range_constraint(Never, T, object) always = ConstraintSet.range(Never, T, object)
# revealed: ty_extensions.ConstraintSet[always] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(always) reveal_type(always)
static_assert(always) static_assert(always)
@ -721,11 +721,11 @@ intersections whose elements appear in different orders.
```py ```py
from typing import Never from typing import Never
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
def f[T](): def f[T]():
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] # 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)] # 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" 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 which tests whether one type is a [subtype][subtyping] of another _assuming that the constraints in
a particular constraint set hold_. 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.) considered concrete.)
```py ```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](): def equivalent_to_other_relationships[T]():
static_assert(is_subtype_of(bool, int)) 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(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. 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 ```py
from typing import Never from typing import Never
from ty_extensions import range_constraint from ty_extensions import ConstraintSet
def even_given_constraints[T](): def even_given_constraints[T]():
constraints = range_constraint(Never, T, int) constraints = ConstraintSet.range(Never, T, int)
static_assert(is_subtype_of_given(constraints, bool, int)) static_assert(constraints.implies_subtype_of(bool, int))
static_assert(not is_subtype_of_given(constraints, bool, str)) static_assert(not constraints.implies_subtype_of(bool, str))
def even_given_unsatisfiable_constraints(): def even_given_unsatisfiable_constraints():
static_assert(is_subtype_of_given(False, bool, int)) static_assert(ConstraintSet.never().implies_subtype_of(bool, int))
static_assert(not is_subtype_of_given(False, bool, str)) static_assert(not ConstraintSet.never().implies_subtype_of(bool, str))
``` ```
## Type variables ## Type variables
@ -141,37 +141,37 @@ considering.
```py ```py
from typing import Never 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](): def given_constraints[T]():
static_assert(not is_subtype_of_given(True, T, int)) static_assert(not ConstraintSet.always().implies_subtype_of(T, int))
static_assert(not is_subtype_of_given(True, T, bool)) static_assert(not ConstraintSet.always().implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(True, T, str)) static_assert(not ConstraintSet.always().implies_subtype_of(T, str))
# These are vacuously true; false implies anything # These are vacuously true; false implies anything
static_assert(is_subtype_of_given(False, T, int)) static_assert(ConstraintSet.never().implies_subtype_of(T, int))
static_assert(is_subtype_of_given(False, T, bool)) static_assert(ConstraintSet.never().implies_subtype_of(T, bool))
static_assert(is_subtype_of_given(False, T, str)) static_assert(ConstraintSet.never().implies_subtype_of(T, str))
given_int = range_constraint(Never, T, int) given_int = ConstraintSet.range(Never, T, int)
static_assert(is_subtype_of_given(given_int, T, int)) static_assert(given_int.implies_subtype_of(T, int))
static_assert(not is_subtype_of_given(given_int, T, bool)) static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(given_int, T, str)) static_assert(not given_int.implies_subtype_of(T, str))
given_bool = range_constraint(Never, T, bool) given_bool = ConstraintSet.range(Never, T, bool)
static_assert(is_subtype_of_given(given_bool, T, int)) static_assert(given_bool.implies_subtype_of(T, int))
static_assert(is_subtype_of_given(given_bool, T, bool)) static_assert(given_bool.implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(given_bool, T, str)) static_assert(not given_bool.implies_subtype_of(T, str))
given_both = given_bool & given_int given_both = given_bool & given_int
static_assert(is_subtype_of_given(given_both, T, int)) static_assert(given_both.implies_subtype_of(T, int))
static_assert(is_subtype_of_given(given_both, T, bool)) static_assert(given_both.implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(given_both, T, str)) static_assert(not given_both.implies_subtype_of(T, str))
given_str = range_constraint(Never, T, str) given_str = ConstraintSet.range(Never, T, str)
static_assert(not is_subtype_of_given(given_str, T, int)) static_assert(not given_str.implies_subtype_of(T, int))
static_assert(not is_subtype_of_given(given_str, T, bool)) static_assert(not given_str.implies_subtype_of(T, bool))
static_assert(is_subtype_of_given(given_str, T, str)) static_assert(given_str.implies_subtype_of(T, str))
``` ```
This might require propagating constraints from other typevars. This might require propagating constraints from other typevars.
@ -179,20 +179,20 @@ This might require propagating constraints from other typevars.
```py ```py
def mutually_constrained[T, U](): def mutually_constrained[T, U]():
# If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. # 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 # TODO: no static-assert-error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_subtype_of_given(given_int, T, int)) static_assert(given_int.implies_subtype_of(T, int))
static_assert(not is_subtype_of_given(given_int, T, bool)) static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(given_int, T, str)) static_assert(not given_int.implies_subtype_of(T, str))
# If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. # 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 # TODO: no static-assert-error
# error: [static-assert-error] # error: [static-assert-error]
static_assert(is_subtype_of_given(given_int, T, int)) static_assert(given_int.implies_subtype_of(T, int))
static_assert(not is_subtype_of_given(given_int, T, bool)) static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not is_subtype_of_given(given_int, T, str)) 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 [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() 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) Type::ClassLiteral(class)
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
{ {
@ -4833,51 +4866,6 @@ impl<'db> Type<'db> {
) )
.into(), .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) => { Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single( Binding::single(
self, self,
@ -6918,7 +6906,14 @@ impl<'db> Type<'db> {
| Type::AlwaysTruthy | Type::AlwaysTruthy
| Type::AlwaysFalsy | Type::AlwaysFalsy
| Type::WrapperDescriptor(_) | 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::DataclassDecorator(_)
| Type::DataclassTransformer(_) | Type::DataclassTransformer(_)
// A non-generic class never needs to be specialized. A generic class is specialized // 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::AlwaysFalsy
| Type::WrapperDescriptor(_) | Type::WrapperDescriptor(_)
| Type::KnownBoundMethod( | Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen, KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) )
| Type::DataclassDecorator(_) | Type::DataclassDecorator(_)
| Type::DataclassTransformer(_) | Type::DataclassTransformer(_)
@ -10318,6 +10318,12 @@ pub enum KnownBoundMethodType<'db> {
StrStartswith(StringLiteralType<'db>), StrStartswith(StringLiteralType<'db>),
/// Method wrapper for `Path.open`, /// Method wrapper for `Path.open`,
PathOpen, PathOpen,
// ConstraintSet methods
ConstraintSetRange,
ConstraintSetAlways,
ConstraintSetNever,
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
} }
pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( 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) => { KnownBoundMethodType::StrStartswith(string_literal) => {
visitor.visit_type(db, Type::StringLiteral(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) ConstraintSet::from(self == other)
} }
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => { (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
ConstraintSet::from(true) | (
} KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
| (
KnownBoundMethodType::ConstraintSetAlways,
KnownBoundMethodType::ConstraintSetAlways,
)
| (
KnownBoundMethodType::ConstraintSetNever,
KnownBoundMethodType::ConstraintSetNever,
)
| (
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(true),
( (
KnownBoundMethodType::FunctionTypeDunderGet(_) KnownBoundMethodType::FunctionTypeDunderGet(_)
@ -10403,13 +10427,21 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen, | KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::FunctionTypeDunderGet(_) KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen, | KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(false), ) => ConstraintSet::from(false),
} }
} }
@ -10445,9 +10477,26 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other) ConstraintSet::from(self == other)
} }
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => { (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
ConstraintSet::from(true) | (
} 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(_) KnownBoundMethodType::FunctionTypeDunderGet(_)
@ -10455,13 +10504,21 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen, | KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::FunctionTypeDunderGet(_) KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen, | KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
) => ConstraintSet::from(false), ) => ConstraintSet::from(false),
} }
} }
@ -10480,7 +10537,12 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PropertyDunderSet(property) => { KnownBoundMethodType::PropertyDunderSet(property) => {
KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor)) 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::PropertyDunderSet(_) => KnownClass::MethodWrapperType,
KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType,
KnownBoundMethodType::PathOpen => KnownClass::MethodType, KnownBoundMethodType::PathOpen => KnownClass::MethodType,
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet,
} }
} }
@ -10592,6 +10658,45 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PathOpen => { KnownBoundMethodType::PathOpen => {
Either::Right(std::iter::once(Signature::todo("`Path.open` return type"))) 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) => { Some(KnownFunction::IsAssignableTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
let constraints = 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) { Type::ClassLiteral(class) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() { Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)), [Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),

View File

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

View File

@ -523,6 +523,18 @@ impl Display for DisplayRepresentation<'_> {
Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => { Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => {
f.write_str("bound method `Path.open`") 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) => { Type::WrapperDescriptor(kind) => {
let (method, object) = match kind { let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),

View File

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

View File

@ -44,6 +44,29 @@ type JustComplex = TypeOf[1.0j]
# Constraints # Constraints
class ConstraintSet: 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 __bool__(self) -> bool: ...
def __eq__(self, other: ConstraintSet) -> bool: ... def __eq__(self, other: ConstraintSet) -> bool: ...
def __ne__(self, other: ConstraintSet) -> bool: ... def __ne__(self, other: ConstraintSet) -> bool: ...
@ -51,13 +74,6 @@ class ConstraintSet:
def __or__(self, other: ConstraintSet) -> ConstraintSet: ... def __or__(self, other: ConstraintSet) -> ConstraintSet: ...
def __invert__(self) -> 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 # Predicates on types
# #
# Ideally, these would be annotated using `TypeForm`, but that has not been # 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 .. _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: def is_assignable_to(ty: Any, to: Any) -> ConstraintSet:
"""Returns a constraint set that is satisfied when `ty` is `assignable`_ to `to`. """Returns a constraint set that is satisfied when `ty` is `assignable`_ to `to`.