## Summary
This PR adds support for 'dangling' `type(...)` constructors, e.g.:
```python
class Foo(type("Bar", ...)):
...
```
As opposed to:
```python
Bar = type("Bar", ...)
```
The former doesn't have a `Definition` since it doesn't get bound to a
place, so we instead need to store the `NodeIndex`. Per @MichaReiser's
suggestion, we can use a Salsa tracked struct for this.
## Summary
Since we've already filtered the union in these locations, it seems like
needless overhead to then intersect the previous union with the filtered
union. We know what that intersection will simplify to: it will simplify
to the filtered union. So rather than using a regular intersection-based
constraint, we can use a "typeguard constraint", which will just
directly replace the previous type with the new type instead of creating
an intersection.
## Test Plan
- Existing tests all pass
- The primer report should be clean
This fixes issue #2470 where recursive type aliases like `type
RecursiveT = int | tuple[RecursiveT, ...]` caused a stack overflow when
used in return type checking with constructors like `list()`.
The fix moves all type mapping processing for `UniqueSpecialization`
(and other non-EagerExpansion mappings) inside the `visitor.visit()`
closure. This ensures that if we encounter the same TypeAlias
recursively during type mapping, the cycle detector will properly detect
it and return the fallback value instead of recursing infinitely.
The key insight is that the previous code called
`apply_function_specialization` followed by another
`apply_type_mapping_impl` AFTER the visitor closure returned. At that
point, the TypeAlias was no longer in the visitor's `seen` set, so
recursive references would not be detected as cycles.
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
-->
## Summary
The test I've added illustrates the fix. Copying it here too:
```python
from contextlib import contextmanager
from typing import Iterator
from typing_extensions import Self
class Base:
@classmethod
@contextmanager
def create(cls) -> Iterator[Self]:
yield cls()
class Child(Base): ...
with Base.create() as base:
reveal_type(base) # revealed: Base (after the fix, None before)
with Child.create() as child:
reveal_type(child) # revealed: Child (after the fix, None before)
```
Full disclosure: I've used LLMs for this PR, but the result is
thoroughly reviewed by me before submitting. I'm excited about my first
Rust contribution to Astral tools and will address feedback quickly.
Related to https://github.com/astral-sh/ty/issues/2030, I am working on
a fix for the TypeVar case also reported in that issue (by me)
<!-- What's the purpose of the change? What does it do, and why? -->
## Test Plan
<!-- How was it tested? -->
Updated mdtests
---------
Co-authored-by: Douglas Creager <dcreager@dcreager.net>
## Summary
I didn't want to make the "dynamic" `type(...)` PR any larger, but it
probably makes sense to rename these now that we have `Dynamic`
variants.
Fixes https://github.com/astral-sh/ty/issues/2467
When calling a method on an instance of a generic class with bounded
type parameters (e.g., `C[T: K]` where `K` is a NewType), ty was
incorrectly reporting: "Argument type `C[K]` does not satisfy upper
bound `C[T@C]` of type variable `Self`"
The issue was introduced by PR #22105, which moved the catch-all case
for NewType assignments that falls back to the concrete base type. This
case was moved before the TypeVar handling cases, so when checking `K <:
T@C` (where K is a NewType and T@C is a TypeVar with upper bound K):
1. The NewType fallback matched first
2. It delegated to `int` (K's concrete base type)
3. Then checked `int <: T@C`, which checks if `int` satisfies bound `K`
4. But `int` is not assignable to `K` (NewTypes are distinct from their
bases)
The fix moves the NewType fallback case after the TypeVar cases, so
TypeVar handling takes precedence. Now when checking `K <: T@C`, we use
the TypeVar case at line 828 which returns `false` for non-inferable
typevars - but this is correct because the *other* direction (`T@C <:
K`) passes, and for the overall specialization comparison both
directions are checked.
## Summary
This PR adds support for dynamic classes created via `type()`. The core
of the change is that `ClassLiteral` is now an enum:
```rust
pub enum ClassLiteral<'db> {
/// A class defined via a `class` statement.
Stmt(StmtClassLiteral<'db>),
/// A class created via the functional form `type(name, bases, dict)`.
Functional(FunctionalClassLiteral<'db>),
}
```
And, in turn, various methods on `ClassLiteral` like `body_scope` now
return `Option` or similar (and callers must adjust to that change in
signature).
Over time, we can expand the enum to include functional namedtuples,
etc. (I already have this working in a separate branch, and I believe it
slots in well.)
(I'd love help with the names -- I think `StmtClassLiteral` is kind of
lame. Maybe `DeclarativeClassLiteral`?)
Closes https://github.com/astral-sh/ty/issues/740.
---------
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
## Summary
The type inference system already correctly special-cases `__file__` to
return `str` for the current module (since the code is executing from an
existing file). However, the completion system was bypassing this logic
and pulling `__file__: str | None` directly from `types.ModuleType` in
typeshed.
This PR adds implicit module globals (like `__file__`, `__name__`, etc.)
with their correctly-typed values to completions, reusing the existing
`module_type_implicit_global_symbol` function that already handles the
special-casing.
Closes https://github.com/astral-sh/ty/issues/2445.
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
## Summary
Like `ProtocolInstance`, we now use `left.cmp(right)` by deriving
`PartialOrd` and `Ord`. IIUC, this uses Salsa ID for Salsa-interned
types, but avoids `None.cmp(None)` for synthesized variants.
Closes https://github.com/astral-sh/ty/issues/2451.
## Summary
Correctly handle upper bounds for contravariant type variables during
specialization inference. Previously, the type checker incorrectly
applied covariant subtyping rules, requiring the actual type to directly
satisfy the bound rather than checking for a valid intersection.
In contravariant positions, subtyping relationships are inverted. The
bug caused valid code like `f(x: Contra[str])` where `f` expects
`Contra[T: int]` to be incorrectly rejected, when it should solve `T` to
`Never` (the intersection of `int` and `str`).
Closes https://github.com/astral-sh/ty/issues/2427
## Details
- Added `is_contravariant()` helper to `TypeVarVariance` in
`variance.rs`
- Updated `SpecializationBuilder::infer_map_impl` in `generics.rs` to
treat bounds and constraints differently based on variance:
* Skip immediate `ty <: bound` check for contravariant upper bounds
* Flip constraint check to `constraint <: ty` for contravariant
positions
- Added test case for bounded contravariant type variables in
`variance.md`
- All 308 mdtest cases pass & 150 ty_python_semantic unit tests pass
---------
Co-authored-by: Douglas Creager <dcreager@dcreager.net>
## Summary
If parent violates LSP against grandparent, and child has the same
violation (but matches parent), we no longer flag the LSP violation on
child, since it can't be fixed without violating parent.
If parent violates LSP against grandparent, and child violates LSP
against both parent and grandparent, we emit two diagnostics (one for
each violation).
If parent violates LSP against grandparent, and child violates LSP
against parent (but not grandparent), we flag it.
Closes https://github.com/astral-sh/ty/issues/2000.
I want to be able to attach extra data to each `Completion`, but not
burden callers with the need to construct it. This commit helps get us
to that point by requiring callers to use a `CompletionBuilder` for
construction instead of a `Completion` itself.
I think this will also help in the future if it proves to be the case
that we can improve performance by delaying work until we actually build
a `Completion`, which might only happen if we know we won't throw it
out. But we aren't quite there yet.
This also lets us tighten things up a little bit and makes completion
construction less noisy. The downside is that callers no longer need to
consider "every" completion field.
There should not be any behavior changes here.
This TODO is very old -- we have long since recorded this definition.
Updating the test to actually assert the declaration requires a new
helper method for declarations, to complement the existing
`first_public_binding` helper.
---------
Co-authored-by: Claude <noreply@anthropic.com>
When working with constraint sets, we track transitive relationships
between the constraints in the set. For instance, in `S ≤ int ∧ int ≤
T`, we can infer that `S ≤ T`. However, we should only consider fully
static types when looking for a "pivot" for this kind of transitive
relationship. The same pattern does not hold for `S ≤ Any ∧ Any ≤ T`;
because the two `Any`s can materialize to different types, we cannot
infer that `S ≤ T`.
Fixes https://github.com/astral-sh/ty/issues/2371
## Summary
This PR fixes `super()` handling when the first parameter (`self` or
`cls`) is annotated with a TypeVar, like `Self`.
Previously, `super()` would incorrectly resolve TypeVars to their bounds
before creating the `BoundSuperType`. So if you had `self: Self` where
`Self` is bounded by `Parent`, we'd process `Parent` as a
`NominalInstance` and end up with `SuperOwnerKind::Instance(Parent)`.
As a result:
```python
class Parent:
@classmethod
def create(cls) -> Self:
return cls()
class Child(Parent):
@classmethod
def create(cls) -> Self:
return super().create() # Error: Argument type `Self@create` does not satisfy upper bound `Parent`
```
We now track two additional variants on `SuperOwnerKind` for TypeVar
owners:
- `InstanceTypeVar`: for instance methods where self is a TypeVar (e.g.,
`self: Self`).
- `ClassTypeVar`: for classmethods where `cls` is a `TypeVar` wrapped in
`type[...]` (e.g., `cls: type[Self]`).
Closes https://github.com/astral-sh/ty/issues/2122.
---------
Co-authored-by: Carl Meyer <carl@astral.sh>
## Summary
Fixes https://github.com/astral-sh/ty/issues/2363
Fixes https://github.com/astral-sh/ty/issues/2013
And several other bugs with the same root cause. And makes any similar
bugs impossible by construction.
Previously we distinguished "no annotation" (Rust `None`) from
"explicitly annotated with something of type `Unknown`" (which is not an
error, and results in the annotation being of Rust type
`Some(Type::DynamicType(Unknown))`), even though semantically these
should be treated the same.
This was a bit of a bug magnet, because it was easy to forget to make
this `None` -> `Unknown` translation everywhere we needed to. And in
fact we did fail to do it in the case of materializing a callable,
leading to a top-materialized callable still having (rust) `None` return
type, which should have instead materialized to `object`.
This also fixes several other bugs related to not handling un-annotated
return types correctly:
1. We previously considered the return type of an unannotated `async
def` to be `Unknown`, where it should be `CoroutineType[Any, Any,
Unknown]`.
2. We previously failed to infer a ParamSpec if the return type of the
callable we are inferring against was not annotated.
3. We previously wrongly returned `Unknown` from `some_dict.get("key",
None)` if the value type of `some_dict` included a callable type with
un-annotated return type.
We now make signature return types and annotated parameter types
required, and we eagerly insert `Unknown` if there's no annotation. Most
of the diff is just a bunch of mechanical code changes where we
construct these types, and simplifications where we use them.
One exception is type display: when a callable type has un-annotated
parameters, we want to display them as un-annotated, but if it has a
parameter explicitly annotated with something of `Unknown` type, we want
to display that parameter as `x: Unknown` (it would be confusing if it
looked like your annotation just disappeared entirely).
Fortunately, we already have a mechanism in place for handling this: the
`inferred_annotation` flag, which suppresses display of an annotation.
Previously we used it only for `self` and `cls` parameters with an
inferred annotated type -- but we now also set it for any un-annotated
parameter, for which we infer `Unknown` type.
We also need to normalize `inferred_annotation`, since it's display-only
and shouldn't impact type equivalence. (This is technically a
previously-existing bug, it just never came up when it only affected
self types -- now it comes up because we have tests asserting that `def
f(x)` and `def g(x: Unknown)` are equivalent.)
## Test Plan
Added mdtests.
## Summary
I wondered if this might improve performance a little. It doesn't seem
to, but it's a net reduction in LOC and I think the changes make sense.
I think it's worth it anyway just in terms of simplifying the code.
## Test Plan
Our existing tests all pass and the primer report is clean (aside from
our usual flakes).
## Summary
fixes: https://github.com/astral-sh/ty/issues/2027
This PR fixes a bug where the type mapping for a `ParamSpec` was not
being applied in an overloaded function.
This PR also fixes https://github.com/astral-sh/ty/issues/2081 and
reveals new diagnostics which doesn't look related to the bug:
```py
from prefect import flow, task
@task
def task_get() -> int:
"""Task get integer."""
return 42
@task
def task_add(x: int, y: int) -> int:
"""Task add two integers."""
print(f"Adding {x} and {y}")
return x + y
@flow
def my_flow():
"""My flow."""
x = 23
future_y = task_get.submit()
# error: [no-matching-overload]
task_add(future_y, future_y)
# error: [no-matching-overload]
task_add(x, future_y)
```
The reason is that the type of `future_y` is `PrefectFuture[int]` while
the type of `task_add` is `Task[(x: int, y: int), int]` which means that
the assignment between `int` and `PrefectFuture[int]` fails which
results in no overload matching. Pyright also raises the invalid
argument type error on all three usages of `future_y` in those two
calls.
## Test Plan
Add regression mdtest from the linked issue.
@dhruvmanila encountered this in #22416 — there are two different
`TypeMapping` variants for apply a specialization to a type. One
operates on a full `Specialization` instance, the other on a partially
constructed one. If we move this enum-ness "down a level" it reduces
some copy/paste in places where we are operating on a `TypeMapping`.
## Summary
Fixes https://github.com/astral-sh/ty/issues/2292
When solving a bounded typevar, we preferred the upper bound over the
actual type seen in the call. This change fixes that.
## Test Plan
Added mdtest, existing tests pass.