## Summary
As-is, the following rejects `return self.value` in `def other` in the
subclass
([link](https://play.ty.dev/f55b47b2-313e-45d1-ba45-fde410bed32e))
because `self.value` is resolving to `Unknown | int | float | property`:
```python
class Base:
_value: float = 0.0
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, v: float) -> None:
self._value = v
@property
def other(self) -> float:
return self.value
@other.setter
def other(self, v: float) -> None:
self.value = v
class Derived(Base):
@property
def other(self) -> float:
return self.value
@other.setter
def other(self, v: float) -> None:
reveal_type(self.value) # revealed: int | float
self.value = v
```
I believe the root cause is that we're not excluding properties when
searching for class methods, so we're treating the `other` setter as a
classmethod. I don't fully understand how that ends up materializing as
`| property` on the union though.
## Summary
If we match on an `TestEnum | None`, then when adding a case like
`~Literal[TestEnum.FOO]` (i.e., after `if value == TestEnum.FOO:
return`), we'd distribute `Literal[TestEnum.BAR]` on the entire builder,
creating `None & Literal[TestEnum.BAR]` which simplified to `Never`.
Instead, we should only expand to the remaining members for pieces of
the intersection that contain the enum.
Now, `(TestEnum | None) & ~Literal[TestEnum.FOO] &
~Literal[TestEnum.BAR]` correctly simplifies to `None` instead of
`Never`.
Closes https://github.com/astral-sh/ty/issues/2260.
## Summary
`apply_type_mapping` always expands type aliases and operates on the
resulting types, which can lead to cluttered results due to excessive
type alias expansion in places where it is not actually needed.
Specifically, type aliases are expanded when displaying method
signatures, because we use `TypeMapping::BindSelf` to get the method
signature.
```python
type Scalar = int | float
type Array1d = list[Scalar] | tuple[Scalar]
def f(x: Scalar | Array1d) -> None: pass
reveal_type(f) # revealed: def f(x: Scalar | Array1d) -> None
class Foo:
def f(self, x: Scalar | Array1d) -> None: pass
# should be `bound method Foo.f(x: Scalar | Array1d) -> None`
reveal_type(Foo().f) # revealed: bound method Foo.f(x: int | float | list[int | float] | tuple[int | float]) -> None
```
In this PR, when type mapping is performed on a type alias, the
expansion result without type mapping is compared with the expansion
result after type mapping, and if the two are equivalent, the expansion
is deemed redundant and canceled.
## Test Plan
mdtest updated
## Summary
Resolve(s) astral-sh/ty#117, astral-sh/ty#1569
Implement `typing.TypeGuard`. Due to the fact that it [overrides
anything previously known about the checked
value](https://typing.python.org/en/latest/spec/narrowing.html#typeguard)---
> When a conditional statement includes a call to a user-defined type
guard function, and that function returns true, the expression passed as
the first positional argument to the type guard function should be
assumed by a static type checker to take on the type specified in the
TypeGuard return type, unless and until it is further narrowed within
the conditional code block.
---we have to substantially rework the constraints system. In
particular, we make constraints represented as a disjunctive normal form
(DNF) where each term includes a regular constraint, and one or more
disjuncts with a typeguard constraint. Some test cases (including some
with more complex boolean logic) are added to `type_guards.md`.
## Test Plan
- update existing tests
- add new tests for more complex boolean logic with `TypeGuard`
- add new tests for `TypeGuard` variance
---------
Co-authored-by: Carl Meyer <carl@astral.sh>
## Summary
I only noticed this in the ecosystem report of
https://github.com/astral-sh/ruff/pull/22213 after merging it. The
change to displaying `Top[]` wrapper around the entire signature instead
of just the parameters had the side effect of not showing it at all when
displaying a top ParamSpec specialization. This PR fixes that.
Marking internal since this is a fixup of a not-released PR.
## Test Plan
Added mdtest that fails without this PR.
## Summary
A couple things I noticed when taking another look at the callable type
materializations.
1) Previously we wrongly ignored the return type when
bottom-materializing a callable with gradual signature, and always
changed it to `Never`.
2) We weren't correctly handling overloads that included a gradual
signature. Rather than separately materializing each overload, we would
just mark the entire callable as "top" or replace the entire callable
with the bottom signature.
Really, "top parameters" is something that belongs on the `Parameters`,
not on the entire `CallableType`. Conveniently, we already have
`ParametersKind` where we can track this, right next to where we already
track `ParametersKind::Gradual`. This saves a bit of memory, fixes the
two bugs above, and simplifies the implementation considerably (net
removal of 100+ LOC, a bunch of places that shouldn't need to care about
topness of a callable no longer need to.)
One user-visible change from this is that I now display the "top
callable" as `(Top[...]) -> object` instead of `Top[(...) -> object]`. I
think this is a (minor) improvement, because it wraps exactly the part
in `Top` that needs to be, rather than misleadingly wrapping the entire
callable type, including the return type (which has already been
separately materialized). I think the prior display would be
particularly confusing if the return type also has its own `Top` in it:
previously we could have e.g. `Top[(...) -> Top[list[Unknown]]]`, which
I think is less clear than the new `(Top[...]) -> Top[list[Unknown]]`.
## Test Plan
Added mdtests that failed before this PR and pass after it.
### Ecosystem
The changed diagnostics are all either the change to `Top` display, or
else known non-deterministic output. The added diagnostics are all true
positives:
The added diagnostic at
aa35ca1965/torchvision/transforms/v2/_utils.py (L149)
is a true positive that wasn't caught by the previous version. `str` is
not assignable to `Callable[[Any], Any]` (strings are not callable), nor
is the top callable (top callable includes callables that do not take a
single required positional argument.)
The added diagnostic at
081535ad9b/starlette/routing.py (L67)
is also a (pedantic) true positive. It's the same case as #1567 -- the
code assumes that it is impossible for a subclass of `Response` to
implement `__await__` (yielding something other than a `Response`).
The pytest added diagnostics are also both similar true positives: they
make the assumption that an object cannot simultaneously be a `Sequence`
and callable, or an `Iterable` and callable.
## Summary
Resolve https://github.com/astral-sh/ty/issues/2226
We need to add a special case in `apply_type_mapping` instead of
directly in `promote_literals_impl` because we do not reach this with
non generic non tuple nominal instances. We still ensure we apply the
normal mapping if we do not see `float` or `complex` instances.
## Test Plan
Update existing mdtest and add a new case to `literal_promotion.md`
## Summary
Other type checkers allow you to access all `FunctionType` attributes on
any object with a `Callable` type. ty does not, because this is
demonstrably unsound, but this is often a source of confusion for users.
And there were lots of diagnostics in the ecosystem report for
https://github.com/astral-sh/ruff/pull/22145 that were complaining that
"Object of type `(...) -> Unknown` has no attribute `__name__`", for
example.
The discrepancy between what ty does here and what other type checkers
do is discussed a bit in https://github.com/astral-sh/ty/issues/1495.
You can see that there have been lots of issues closed as duplicates of
that issue; we should probably also add an FAQ entry for it.
Anyway, this PR adds a subdiagnostic to help users out when they hit
this diagnostic. Unfortunately something I did meant that rustfmt
increased the indentation of the whole of this huge closure, so this PR
is best reviewed with the "No whitespace" option selected for viewing
the diff.
## Test Plan
Snapshot added
## Summary
Provides a message like:
```
error[invalid-argument-type]: Cannot delete required key "name" from TypedDict `Movie`
--> test.py:15:7
|
15 | del m["name"]
| ^^^^^^
|
info: Field defined here
--> test.py:4:5
|
4 | name: str
| --------- `name` declared as required here; consider making it `NotRequired`
|
info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted
```
## Summary
TypedDict now synthesizes a proper `__delitem__` method that...
- ...allows deletion of `NotRequired` keys and keys in `total=False`
TypedDicts.
- ...rejects deletion of required keys (synthesizes `__delitem__(k:
Never)`).
## Summary
We already had `CallableTypeKind::ClassMethodLike` to track callables
that behave like `classmethods` (always bind the first argument). This
PR adds the symmetric `CallableTypeKind::StaticMethodLike` for callables
that behave like `staticmethods` (never bind `self`).
Closes https://github.com/astral-sh/ty/issues/2114.
## Summary
Previously, `del x[k]` incorrectly required the object to have a
`__getitem__` method. This was wrong because deletion only needs
`__delitem__`, which is independent of `__getitem__`.
Closes https://github.com/astral-sh/ty/issues/1799.
## Summary
This PR fixes https://github.com/astral-sh/ty/issues/1848.
```python
T = tuple[int, 'U']
class C(set['U']):
pass
type U = T | C
```
The reason why the fixed point iteration did not converge was because
the types stored in the implicit tuple type alias `Specialization`
changed each time.
```
1st: <class 'tuple[int, C]'>
2nd: <class 'tuple[int, tuple[int, C] | C]'>
3rd: <class 'tuple[int, tuple[int, tuple[int, C] | C] | C]'>
...
```
And this was because `UnionType::from_elements` was used when creating
union types for tuple operations, which causes type aliases inside to be
expanded.
This PR replaces these with `UnionType::from_elements_leave_aliases`.
## Test Plan
New corpus test
Identify and narrow cases like this:
```py
class Foo(TypedDict):
tag: Literal["foo"]
class Bar(TypedDict):
tag: Literal["bar"]
def _(union: Foo | Bar):
if union["tag"] == "foo":
reveal_type(union) # Foo
```
Fixes part of https://github.com/astral-sh/ty/issues/1479.
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>