[ty] defer inference of legacy TypeVar bound/constraints/defaults (#20598)

## Summary

This allows us to handle self-referential bounds/constraints/defaults
without panicking.

Handles more cases from https://github.com/astral-sh/ty/issues/256

This also changes the way we infer the types of legacy TypeVars. Rather
than understanding a constructor call to `typing[_extension].TypeVar`
inside of any (arbitrarily nested) expression, and having to use a
special `assigned_to` field of the semantic index to try to best-effort
figure out what name the typevar was assigned to, we instead understand
the creation of a legacy `TypeVar` only in the supported syntactic
position (RHS of a simple un-annotated assignment with one target). In
any other position, we just infer it as creating an opaque instance of
`typing.TypeVar`. (This behavior matches all other type checkers.)

So we now special-case TypeVar creation in `TypeInferenceBuilder`, as a
special case of an assignment definition, rather than deeper inside call
binding. This does mean we re-implement slightly more of
argument-parsing, but in practice this is minimal and easy to handle
correctly.

This is easier to implement if we also make the RHS of a simple (no
unpacking) one-target assignment statement no longer a standalone
expression. Which is fine to do, because simple one-target assignments
don't need to infer the RHS more than once. This is a bonus performance
(0-3% across various projects) and significant memory-usage win, since
most assignment statements are simple one-target assignment statements,
meaning we now create many fewer standalone-expression salsa
ingredients.

This change does mean that inference of manually-constructed
`TypeAliasType` instances can no longer find its Definition in
`assigned_to`, which regresses go-to-definition for these aliases. In a
future PR, `TypeAliasType` will receive the same treatment that
`TypeVar` did in this PR (moving its special-case inference into
`TypeInferenceBuilder` and supporting it only in the correct syntactic
position, and lazily inferring its value type to support recursion),
which will also fix the go-to-definition regression. (I decided a
temporary edge-case regression is better in this case than doubling the
size of this PR.)

This PR also tightens up and fixes various aspects of the validation of
`TypeVar` creation, as seen in the tests.

We still (for now) treat all typevars as instances of `typing.TypeVar`,
even if they were created using `typing_extensions.TypeVar`. This means
we'll wrongly error on e.g. `T.__default__` on Python 3.11, even if `T`
is a `typing_extensions.TypeVar` instance at runtime. We share this
wrong behavior with both mypy and pyrefly. It will be easier to fix
after we pull in https://github.com/python/typeshed/pull/14840.

There are some issues that showed up here with typevar identity and
`MarkTypeVarsInferable`; the fix here (using the new `original` field
and `is_identical_to` methods on `BoundTypeVarInstance` and
`TypeVarInstance`) is a bit kludgy, but it can go away when we eliminate
`MarkTypeVarsInferable`.

## Test Plan

Added and updated mdtests.

### Conformance suite impact

The impact here is all positive:

* We now correctly error on a legacy TypeVar with exactly one constraint
type given.
* We now correctly error on a legacy TypeVar with both an upper bound
and constraints specified.

### Ecosystem impact

Basically none; in the setuptools case we just issue slightly different
errors on an invalid TypeVar definition, due to the modified validation
code.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Carl Meyer 2025-10-09 14:08:37 -07:00 committed by GitHub
parent b086ffe921
commit 8248193ed9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1441 additions and 408 deletions

View file

@ -308,26 +308,8 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:1
|
2 | from typing_extensions import TypeAliasType
3 |
4 | Alias = TypeAliasType("Alias", tuple[int, int])
| ^^^^^
5 |
6 | Alias
|
info: Source
--> main.py:6:1
|
4 | Alias = TypeAliasType("Alias", tuple[int, int])
5 |
6 | Alias
| ^^^^^
|
"#);
// TODO: This should jump to the definition of `Alias` above.
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]

View file

@ -0,0 +1,139 @@
# Legacy typevar creation diagnostics
The full tests for these features are in `generics/legacy/variables.md`.
<!-- snapshot-diagnostics -->
## Must have a name
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar()
```
## Name can't be given more than once
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", name="T")
```
## Must be directly assigned to a variable
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
> part of a larger expression).
```py
from typing import TypeVar
T = TypeVar("T")
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable]
tuple_with_typevar = ("foo", TypeVar("W"))
```
## `TypeVar` parameter must match variable name
> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned.
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("Q")
```
## No variadic arguments
```py
from typing import TypeVar
types = (int, str)
# error: [invalid-legacy-type-variable]
T = TypeVar("T", *types)
# error: [invalid-legacy-type-variable]
S = TypeVar("S", **{"bound": int})
```
## Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
> be at least two constraints, if any; specifying a single constraint is disallowed.
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", int)
```
## Cannot have both bound and constraint
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", int, str, bound=bytes)
```
## Cannot be both covariant and contravariant
> To facilitate the declaration of container types where covariant or contravariant type checking is
> acceptable, type variables accept keyword arguments `covariant=True` or `contravariant=True`. At
> most one of these may be passed.
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=True, contravariant=True)
```
## Boolean parameters must be unambiguous
```py
from typing_extensions import TypeVar
def cond() -> bool:
return True
# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=cond())
# error: [invalid-legacy-type-variable]
U = TypeVar("U", contravariant=cond())
# error: [invalid-legacy-type-variable]
V = TypeVar("V", infer_variance=cond())
```
## Invalid keyword arguments
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", invalid_keyword=True)
```
## Invalid feature for this Python version
```toml
[environment]
python-version = "3.10"
```
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", default=int)
```

View file

@ -108,7 +108,7 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTyp
The type parameter can be specified explicitly:
```py
from typing import Generic, Literal, TypeVar
from typing_extensions import Generic, Literal, TypeVar
T = TypeVar("T")
@ -195,7 +195,7 @@ reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
We can infer the type parameter from a type context:
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -240,7 +240,7 @@ consistent with each other.
### `__new__` only
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -257,7 +257,7 @@ wrong_innards: C[int] = C("five")
### `__init__` only
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -273,7 +273,7 @@ wrong_innards: C[int] = C("five")
### Identical `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -292,7 +292,7 @@ wrong_innards: C[int] = C("five")
### Compatible `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -325,7 +325,7 @@ If either method comes from a generic base class, we don't currently use its inf
to specialize the class.
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -344,7 +344,7 @@ reveal_type(D(1)) # revealed: D[int]
### Generic class inherits `__init__` from generic base class
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -364,7 +364,7 @@ reveal_type(D(1, "str")) # revealed: D[int, str]
This is a specific example of the above, since it was reported specifically by a user.
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -382,7 +382,7 @@ for `tuple`, so we use a different mechanism to make sure it has the right inher
context. But from the user's point of view, this is another example of the above.)
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -403,7 +403,7 @@ python-version = "3.11"
```
```py
from typing import TypeVar, Sequence, Never
from typing_extensions import TypeVar, Sequence, Never
T = TypeVar("T")
@ -421,7 +421,7 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
### `__init__` is itself generic
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
S = TypeVar("S")
T = TypeVar("T")
@ -440,7 +440,7 @@ wrong_innards: C[int] = C("five", 1)
### Some `__init__` overloads only apply to certain specializations
```py
from typing import overload, Generic, TypeVar
from typing_extensions import overload, Generic, TypeVar
T = TypeVar("T")
@ -480,7 +480,7 @@ C[None](12)
```py
from dataclasses import dataclass
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -494,7 +494,7 @@ reveal_type(A(x=1)) # revealed: A[int]
### Class typevar has another typevar as a default
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U", default=T)
@ -515,7 +515,7 @@ When a generic subclass fills its superclass's type parameter with one of its ow
propagate through:
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -549,7 +549,7 @@ scope for the method.
```py
from ty_extensions import generic_context
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
@ -581,7 +581,7 @@ In a specialized generic alias, the specialization is applied to the attributes
class.
```py
from typing import Generic, TypeVar, Protocol
from typing_extensions import Generic, TypeVar, Protocol
T = TypeVar("T")
U = TypeVar("U")
@ -639,7 +639,7 @@ reveal_type(d.method3().x) # revealed: int
When a method is overloaded, the specialization is applied to all overloads.
```py
from typing import overload, Generic, TypeVar
from typing_extensions import overload, Generic, TypeVar
S = TypeVar("S")
@ -667,7 +667,7 @@ A class can use itself as the type parameter of one of its superclasses. (This i
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
```pyi
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -682,7 +682,7 @@ reveal_type(Sub) # revealed: <class 'Sub'>
A similar case can work in a non-stub file, if forward references are stringified:
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -697,7 +697,7 @@ reveal_type(Sub) # revealed: <class 'Sub'>
In a non-stub file, without stringified forward references, this raises a `NameError`:
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -710,7 +710,7 @@ class Sub(Base[Sub]): ...
### Cyclic inheritance as a generic parameter
```pyi
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")
@ -722,7 +722,7 @@ class Derived(list[Derived[T]], Generic[T]): ...
Inheritance that would result in a cyclic MRO is detected as an error.
```py
from typing import Generic, TypeVar
from typing_extensions import Generic, TypeVar
T = TypeVar("T")

View file

@ -6,6 +6,8 @@ for both type variable syntaxes.
Unless otherwise specified, all quotations come from the [Generics] section of the typing spec.
Diagnostics for invalid type variables are snapshotted in `diagnostics/legacy_typevars.md`.
## Type variables
### Defining legacy type variables
@ -24,7 +26,16 @@ reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Directly assigned to a variable
The typevar name can also be provided as a keyword argument:
```py
from typing import TypeVar
T = TypeVar(name="T")
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Must be directly assigned to a variable
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
> part of a larger expression).
@ -33,13 +44,24 @@ reveal_type(T.__name__) # revealed: Literal["T"]
from typing import TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
# error: [invalid-type-form] "Function calls are not allowed in type expressions"
TestList = list[TypeVar("W")]
# error: [invalid-legacy-type-variable]
tuple_with_typevar = ("foo", TypeVar("W"))
reveal_type(tuple_with_typevar[1]) # revealed: TypeVar
```
```py
from typing_extensions import TypeVar
T = TypeVar("T")
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable]
tuple_with_typevar = ("foo", TypeVar("W"))
reveal_type(tuple_with_typevar[1]) # revealed: TypeVar
```
### `TypeVar` parameter must match variable name
@ -49,7 +71,7 @@ TestList = list[TypeVar("W")]
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
# error: [invalid-legacy-type-variable]
T = TypeVar("Q")
```
@ -66,6 +88,22 @@ T = TypeVar("T")
T = TypeVar("T")
```
### No variadic arguments
```py
from typing import TypeVar
types = (int, str)
# error: [invalid-legacy-type-variable]
T = TypeVar("T", *types)
reveal_type(T) # revealed: TypeVar
# error: [invalid-legacy-type-variable]
S = TypeVar("S", **{"bound": int})
reveal_type(S) # revealed: TypeVar
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
@ -91,6 +129,11 @@ reveal_type(S.__default__) # revealed: NoDefault
### Using other typevars as a default
```toml
[environment]
python-version = "3.13"
```
```py
from typing import Generic, TypeVar, Union
@ -124,6 +167,15 @@ S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
```
The upper bound must be a valid type expression:
```py
from typing import TypedDict
# error: [invalid-type-form]
T = TypeVar("T", bound=TypedDict)
```
### Type variables with constraints
```py
@ -138,6 +190,16 @@ S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
```
Constraints are not simplified relative to each other, even if one is a subtype of the other:
```py
T = TypeVar("T", int, bool)
reveal_type(T.__constraints__) # revealed: tuple[int, bool]
S = TypeVar("S", float, str)
reveal_type(S.__constraints__) # revealed: tuple[int | float, str]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
@ -146,10 +208,19 @@ reveal_type(S.__constraints__) # revealed: tuple[()]
```py
from typing import TypeVar
# TODO: error: [invalid-type-variable-constraints]
# error: [invalid-legacy-type-variable]
T = TypeVar("T", int)
```
### Cannot have both bound and constraint
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", int, str, bound=bytes)
```
### Cannot be both covariant and contravariant
> To facilitate the declaration of container types where covariant or contravariant type checking is
@ -163,10 +234,10 @@ from typing import TypeVar
T = TypeVar("T", covariant=True, contravariant=True)
```
### Variance parameters must be unambiguous
### Boolean parameters must be unambiguous
```py
from typing import TypeVar
from typing_extensions import TypeVar
def cond() -> bool:
return True
@ -176,6 +247,73 @@ T = TypeVar("T", covariant=cond())
# error: [invalid-legacy-type-variable]
U = TypeVar("U", contravariant=cond())
# error: [invalid-legacy-type-variable]
V = TypeVar("V", infer_variance=cond())
```
### Invalid keyword arguments
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", invalid_keyword=True)
```
```pyi
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", invalid_keyword=True)
```
### Constructor signature versioning
#### For `typing.TypeVar`
```toml
[environment]
python-version = "3.10"
```
In a stub file, features from the latest supported Python version can be used on any version.
There's no need to require use of `typing_extensions.TypeVar` in a stub file, when the type checker
can understand the typevar definition perfectly well either way, and there can be no runtime error.
(Perhaps it's arguable whether this special case is worth it, but other type checkers do it, so we
maintain compatibility.)
```pyi
from typing import TypeVar
T = TypeVar("T", default=int)
```
But this raises an error in a non-stub file:
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", default=int)
```
#### For `typing_extensions.TypeVar`
`typing_extensions.TypeVar` always supports the latest features, on any Python version.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import TypeVar
T = TypeVar("T", default=int)
# TODO: should not error, should reveal `int`
# error: [unresolved-attribute]
reveal_type(T.__default__) # revealed: Unknown
```
## Callability
@ -231,4 +369,96 @@ def constrained(x: T_constrained):
reveal_type(type(x)) # revealed: type[int] | type[str]
```
## Cycles
### Bounds and constraints
A typevar's bounds and constraints cannot be generic, cyclic or otherwise:
```py
from typing import Any, TypeVar
S = TypeVar("S")
# TODO: error
T = TypeVar("T", bound=list[S])
# TODO: error
U = TypeVar("U", list["T"], str)
# TODO: error
V = TypeVar("V", list["V"], str)
```
However, they are lazily evaluated and can cyclically refer to their own type:
```py
from typing import TypeVar, Generic
T = TypeVar("T", bound=list["G"])
class G(Generic[T]):
x: T
reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
```
### Defaults
```toml
[environment]
python-version = "3.13"
```
Defaults can be generic, but can only refer to typevars from the same scope if they were defined
earlier in that scope:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U", default=T)
class C(Generic[T, U]):
x: T
y: U
reveal_type(C[int, str]().x) # revealed: int
reveal_type(C[int, str]().y) # revealed: str
reveal_type(C[int]().x) # revealed: int
reveal_type(C[int]().y) # revealed: int
# TODO: error
V = TypeVar("V", default="V")
class D(Generic[V]):
x: V
# TODO: we shouldn't leak a typevar like this in type inference
reveal_type(D().x) # revealed: V@D
```
## Regression
### Use of typevar with default inside a function body that binds it
```toml
[environment]
python-version = "3.13"
```
```py
from typing import Generic, TypeVar
_DataT = TypeVar("_DataT", bound=int, default=int)
class Event(Generic[_DataT]):
def __init__(self, data: _DataT) -> None:
self.data = data
def async_fire_internal(event_data: _DataT):
event: Event[_DataT] | None = None
event = Event(event_data)
```
[generics]: https://typing.python.org/en/latest/spec/generics.html

View file

@ -197,9 +197,9 @@ from typing_extensions import TypeAliasType, TypeVar
T = TypeVar("T")
IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
def f(x: IntAnd[str]) -> None:
def f(x: IntAndT[str]) -> None:
reveal_type(x) # revealed: @Todo(Generic manual PEP-695 type alias)
```

View file

@ -0,0 +1,70 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Boolean parameters must be unambiguous
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import TypeVar
2 |
3 | def cond() -> bool:
4 | return True
5 |
6 | # error: [invalid-legacy-type-variable]
7 | T = TypeVar("T", covariant=cond())
8 |
9 | # error: [invalid-legacy-type-variable]
10 | U = TypeVar("U", contravariant=cond())
11 |
12 | # error: [invalid-legacy-type-variable]
13 | V = TypeVar("V", infer_variance=cond())
```
# Diagnostics
```
error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness
--> src/mdtest_snippet.py:7:28
|
6 | # error: [invalid-legacy-type-variable]
7 | T = TypeVar("T", covariant=cond())
| ^^^^^^
8 |
9 | # error: [invalid-legacy-type-variable]
|
info: rule `invalid-legacy-type-variable` is enabled by default
```
```
error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness
--> src/mdtest_snippet.py:10:32
|
9 | # error: [invalid-legacy-type-variable]
10 | U = TypeVar("U", contravariant=cond())
| ^^^^^^
11 |
12 | # error: [invalid-legacy-type-variable]
|
info: rule `invalid-legacy-type-variable` is enabled by default
```
```
error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness
--> src/mdtest_snippet.py:13:33
|
12 | # error: [invalid-legacy-type-variable]
13 | V = TypeVar("V", infer_variance=cond())
| ^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot be both covariant and contravariant
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", covariant=True, contravariant=True)
```
# Diagnostics
```
error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant
--> src/mdtest_snippet.py:4:5
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", covariant=True, contravariant=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have both bound and constraint
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", int, str, bound=bytes)
```
# Diagnostics
```
error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints
--> src/mdtest_snippet.py:4:5
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", int, str, bound=bytes)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have only one constraint
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", int)
```
# Diagnostics
```
error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint
--> src/mdtest_snippet.py:4:18
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", int)
| ^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid feature for this Python version
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", default=int)
```
# Diagnostics
```
error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13
--> src/mdtest_snippet.py:4:18
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", default=int)
| ^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid keyword arguments
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", invalid_keyword=True)
```
# Diagnostics
```
error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation
--> src/mdtest_snippet.py:4:18
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", invalid_keyword=True)
| ^^^^^^^^^^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must be directly assigned to a variable
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | T = TypeVar("T")
4 | # error: [invalid-legacy-type-variable]
5 | U: TypeVar = TypeVar("U")
6 |
7 | # error: [invalid-legacy-type-variable]
8 | tuple_with_typevar = ("foo", TypeVar("W"))
```
# Diagnostics
```
error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment
--> src/mdtest_snippet.py:5:14
|
3 | T = TypeVar("T")
4 | # error: [invalid-legacy-type-variable]
5 | U: TypeVar = TypeVar("U")
| ^^^^^^^^^^^^
6 |
7 | # error: [invalid-legacy-type-variable]
|
info: rule `invalid-legacy-type-variable` is enabled by default
```
```
error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment
--> src/mdtest_snippet.py:8:30
|
7 | # error: [invalid-legacy-type-variable]
8 | tuple_with_typevar = ("foo", TypeVar("W"))
| ^^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must have a name
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar()
```
# Diagnostics
```
error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required.
--> src/mdtest_snippet.py:4:5
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar()
| ^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Name can't be given more than once
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", name="T")
```
# Diagnostics
```
error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once.
--> src/mdtest_snippet.py:4:18
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("T", name="T")
| ^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - No variadic arguments
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | types = (int, str)
4 |
5 | # error: [invalid-legacy-type-variable]
6 | T = TypeVar("T", *types)
7 |
8 | # error: [invalid-legacy-type-variable]
9 | S = TypeVar("S", **{"bound": int})
```
# Diagnostics
```
error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation
--> src/mdtest_snippet.py:6:18
|
5 | # error: [invalid-legacy-type-variable]
6 | T = TypeVar("T", *types)
| ^^^^^^
7 |
8 | # error: [invalid-legacy-type-variable]
|
info: rule `invalid-legacy-type-variable` is enabled by default
```
```
error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation
--> src/mdtest_snippet.py:9:18
|
8 | # error: [invalid-legacy-type-variable]
9 | S = TypeVar("S", **{"bound": int})
| ^^^^^^^^^^^^^^^^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -0,0 +1,33 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - `TypeVar` parameter must match variable name
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 |
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("Q")
```
# Diagnostics
```
error[invalid-legacy-type-variable]: The name of a `TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)
--> src/mdtest_snippet.py:4:1
|
3 | # error: [invalid-legacy-type-variable]
4 | T = TypeVar("Q")
| ^
|
info: rule `invalid-legacy-type-variable` is enabled by default
```

View file

@ -1165,10 +1165,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
target: &'ast ast::Expr,
value: Expression<'db>,
) {
// We only handle assignments to names and unpackings here, other targets like
// attribute and subscript are handled separately as they don't create a new
// definition.
let current_assignment = match target {
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
if matches!(unpackable, Unpackable::Comprehension { .. }) {
@ -1628,10 +1624,22 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
debug_assert_eq!(&self.current_assignments, &[]);
self.visit_expr(&node.value);
let value = self.add_standalone_assigned_expression(&node.value, node);
for target in &node.targets {
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
// Optimization for the common case: if there's just one target, and it's not an
// unpacking, and the target is a simple name, we don't need the RHS to be a
// standalone expression at all.
if let [target] = &node.targets[..]
&& target.is_name_expr()
{
self.push_assignment(CurrentAssignment::Assign { node, unpack: None });
self.visit_expr(target);
self.pop_assignment();
} else {
let value = self.add_standalone_assigned_expression(&node.value, node);
for target in &node.targets {
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
}
}
}
ast::Stmt::AnnAssign(node) => {

View file

@ -706,13 +706,6 @@ impl DefinitionKind<'_> {
matches!(self, DefinitionKind::Assignment(_))
}
pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
match self {
DefinitionKind::TypeVar(type_var) => Some(type_var),
_ => None,
}
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the place being defined i.e.,

View file

@ -33,7 +33,7 @@ pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, resolve_module};
use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::{imported_modules, place_table, semantic_index};
@ -1642,7 +1642,9 @@ impl<'db> Type<'db> {
(
Type::NonInferableTypeVar(lhs_bound_typevar),
Type::NonInferableTypeVar(rhs_bound_typevar),
) if lhs_bound_typevar == rhs_bound_typevar => ConstraintSet::from(true),
) if lhs_bound_typevar.is_identical_to(db, rhs_bound_typevar) => {
ConstraintSet::from(true)
}
// A fully static typevar is a subtype of its upper bound, and to something similar to
// the union of its constraints. An unbound, unconstrained, fully static typevar has an
@ -4841,56 +4843,6 @@ impl<'db> Type<'db> {
.into()
}
Some(KnownClass::TypeVar) => {
// ```py
// class TypeVar:
// def __new__(
// cls,
// name: str,
// *constraints: Any,
// bound: Any | None = None,
// contravariant: bool = False,
// covariant: bool = False,
// infer_variance: bool = False,
// default: Any = ...,
// ) -> Self: ...
// ```
Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("name"))
.with_annotated_type(Type::LiteralString),
Parameter::variadic(Name::new_static("constraints"))
.type_form()
.with_annotated_type(Type::any()),
Parameter::keyword_only(Name::new_static("bound"))
.type_form()
.with_annotated_type(UnionType::from_elements(
db,
[Type::any(), Type::none(db)],
))
.with_default_type(Type::none(db)),
Parameter::keyword_only(Name::new_static("default"))
.type_form()
.with_annotated_type(Type::any())
.with_default_type(KnownClass::NoneType.to_instance(db)),
Parameter::keyword_only(Name::new_static("contravariant"))
.with_annotated_type(KnownClass::Bool.to_instance(db))
.with_default_type(Type::BooleanLiteral(false)),
Parameter::keyword_only(Name::new_static("covariant"))
.with_annotated_type(KnownClass::Bool.to_instance(db))
.with_default_type(Type::BooleanLiteral(false)),
Parameter::keyword_only(Name::new_static("infer_variance"))
.with_annotated_type(KnownClass::Bool.to_instance(db))
.with_default_type(Type::BooleanLiteral(false)),
]),
Some(KnownClass::TypeVar.to_instance(db)),
),
)
.into()
}
Some(KnownClass::Deprecated) => {
// ```py
// class deprecated:
@ -7832,6 +7784,12 @@ pub struct TypeVarInstance<'db> {
_default: Option<TypeVarDefaultEvaluation<'db>>,
pub kind: TypeVarKind,
/// If this typevar was transformed from another typevar via `mark_typevars_inferable`, this
/// records the identity of the "original" typevar, so we can recognize them as the same
/// typevar in `bind_typevar`. TODO: this (and the `is_identical_to` methods) should be
/// removable once we remove `mark_typevars_inferable`.
pub(crate) original: Option<TypeVarInstance<'db>>,
}
// The Salsa heap is tracked separately.
@ -7942,6 +7900,7 @@ impl<'db> TypeVarInstance<'db> {
.map(|ty| ty.normalized_impl(db, visitor).into()),
}),
self.kind(db),
self.original(db),
)
}
@ -7987,6 +7946,7 @@ impl<'db> TypeVarInstance<'db> {
.map(|ty| ty.materialize(db, materialization_kind, visitor).into()),
}),
self.kind(db),
self.original(db),
)
}
@ -8000,10 +7960,7 @@ impl<'db> TypeVarInstance<'db> {
// inferable, so we set the parameter to `None` here.
let type_mapping = &TypeMapping::MarkTypeVarsInferable(None);
Self::new(
db,
self.name(db),
self.definition(db),
let new_bound_or_constraints =
self._bound_or_constraints(db)
.map(|bound_or_constraints| match bound_or_constraints {
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
@ -8013,22 +7970,46 @@ impl<'db> TypeVarInstance<'db> {
}
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound
| TypeVarBoundOrConstraintsEvaluation::LazyConstraints => bound_or_constraints,
}),
self.explicit_variance(db),
self._default(db).and_then(|default| match default {
TypeVarDefaultEvaluation::Eager(ty) => Some(
ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor)
.into(),
),
TypeVarDefaultEvaluation::Lazy => self.lazy_default(db).map(|ty| {
ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor)
.into()
}),
});
let new_default = self._default(db).and_then(|default| match default {
TypeVarDefaultEvaluation::Eager(ty) => Some(
ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor)
.into(),
),
TypeVarDefaultEvaluation::Lazy => self.lazy_default(db).map(|ty| {
ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor)
.into()
}),
});
// Ensure that we only modify the `original` field if we are going to modify one or both of
// `_bound_or_constraints` and `_default`; don't trigger creation of a new
// `TypeVarInstance` unnecessarily.
let new_original = if new_bound_or_constraints == self._bound_or_constraints(db)
&& new_default == self._default(db)
{
self.original(db)
} else {
Some(self)
};
Self::new(
db,
self.name(db),
self.definition(db),
new_bound_or_constraints,
self.explicit_variance(db),
new_default,
self.kind(db),
new_original,
)
}
fn is_identical_to(self, db: &'db dyn Db, other: Self) -> bool {
self == other || (self.original(db) == Some(other) || other.original(db) == Some(self))
}
fn to_instance(self, db: &'db dyn Db) -> Option<Self> {
let bound_or_constraints = match self.bound_or_constraints(db)? {
TypeVarBoundOrConstraints::UpperBound(upper_bound) => {
@ -8046,38 +8027,88 @@ impl<'db> TypeVarInstance<'db> {
self.explicit_variance(db),
None,
self.kind(db),
self.original(db),
))
}
#[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial)]
#[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn lazy_bound(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
let ty = match definition.kind(db) {
// PEP 695 typevar
DefinitionKind::TypeVar(typevar) => {
let typevar_node = typevar.node(&module);
definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
}
// legacy typevar
DefinitionKind::Assignment(assignment) => {
let call_expr = assignment.value(&module).as_call_expr()?;
let expr = &call_expr.arguments.find_keyword("bound")?.value;
definition_expression_type(db, definition, expr)
}
_ => return None,
};
Some(TypeVarBoundOrConstraints::UpperBound(ty))
}
#[salsa::tracked]
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
.into_union()?;
let ty = match definition.kind(db) {
// PEP 695 typevar
DefinitionKind::TypeVar(typevar) => {
let typevar_node = typevar.node(&module);
definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
.into_union()?
}
// legacy typevar
DefinitionKind::Assignment(assignment) => {
let call_expr = assignment.value(&module).as_call_expr()?;
// We don't use `UnionType::from_elements` or `UnionBuilder` here,
// because we don't want to simplify the list of constraints as we would with
// an actual union type.
// TODO: We probably shouldn't use `UnionType` to store these at all? TypeVar
// constraints are not a union.
UnionType::new(
db,
call_expr
.arguments
.args
.iter()
.skip(1)
.map(|arg| definition_expression_type(db, definition, arg))
.collect::<Box<_>>(),
)
}
_ => return None,
};
Some(TypeVarBoundOrConstraints::Constraints(ty))
}
#[salsa::tracked]
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
fn lazy_default(self, db: &'db dyn Db) -> Option<Type<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
Some(definition_expression_type(
db,
definition,
typevar_node.default.as_ref()?,
))
match definition.kind(db) {
// PEP 695 typevar
DefinitionKind::TypeVar(typevar) => {
let typevar_node = typevar.node(&module);
Some(definition_expression_type(
db,
definition,
typevar_node.default.as_ref()?,
))
}
// legacy typevar
DefinitionKind::Assignment(assignment) => {
let call_expr = assignment.value(&module).as_call_expr()?;
let expr = &call_expr.arguments.find_keyword("default")?.value;
Some(definition_expression_type(db, definition, expr))
}
_ => None,
}
}
}
@ -8153,6 +8184,7 @@ impl<'db> BoundTypeVarInstance<'db> {
Some(variance),
None, // _default
TypeVarKind::Pep695,
None,
),
BindingContext::Synthetic,
)
@ -8174,11 +8206,24 @@ impl<'db> BoundTypeVarInstance<'db> {
Some(TypeVarVariance::Invariant),
None,
TypeVarKind::TypingSelf,
None,
),
binding_context,
)
}
pub(crate) fn is_identical_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
if self.binding_context(db) != other.binding_context(db) {
return false;
}
self.typevar(db).is_identical_to(db, other.typevar(db))
}
pub(crate) fn variance_with_polarity(
self,
db: &'db dyn Db,

View file

@ -16,7 +16,7 @@ use crate::semantic_index::{
};
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
use crate::types::enums::enum_metadata;
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization, walk_specialization};
@ -29,9 +29,8 @@ use crate::types::{
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType,
MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType,
TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypedDictParams, UnionBuilder, VarianceInferable, declaration_type,
determine_upper_bound, infer_definition_types,
TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable,
declaration_type, determine_upper_bound, infer_definition_types,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
@ -3761,6 +3760,8 @@ pub enum KnownClass {
SupportsIndex,
Iterable,
Iterator,
// typing_extensions
ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features
// Collections
ChainMap,
Counter,
@ -3815,6 +3816,7 @@ impl KnownClass {
| Self::VersionInfo
| Self::TypeAliasType
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
@ -3943,6 +3945,7 @@ impl KnownClass {
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
@ -4025,6 +4028,7 @@ impl KnownClass {
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
@ -4107,6 +4111,7 @@ impl KnownClass {
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
@ -4194,6 +4199,7 @@ impl KnownClass {
| Self::NoneType
| Self::SpecialForm
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
@ -4289,6 +4295,7 @@ impl KnownClass {
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
@ -4358,6 +4365,7 @@ impl KnownClass {
Self::NoneType => "NoneType",
Self::SpecialForm => "_SpecialForm",
Self::TypeVar => "TypeVar",
Self::ExtensionsTypeVar => "TypeVar",
Self::ParamSpec => "ParamSpec",
Self::ParamSpecArgs => "ParamSpecArgs",
Self::ParamSpecKwargs => "ParamSpecKwargs",
@ -4656,6 +4664,7 @@ impl KnownClass {
| Self::ProtocolMeta
| Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType
| Self::ExtensionsTypeVar
| Self::TypeVarTuple
| Self::ParamSpec
| Self::ParamSpecArgs
@ -4752,6 +4761,7 @@ impl KnownClass {
| Self::SupportsIndex
| Self::StdlibAlias
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
@ -4838,6 +4848,7 @@ impl KnownClass {
| Self::Generator
| Self::Deprecated
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
@ -4875,99 +4886,102 @@ impl KnownClass {
) -> Option<Self> {
// We assert that this match is exhaustive over the right-hand side in the unit test
// `known_class_roundtrip_from_str()`
let candidate = match class_name {
"bool" => Self::Bool,
"object" => Self::Object,
"bytes" => Self::Bytes,
"bytearray" => Self::Bytearray,
"tuple" => Self::Tuple,
"type" => Self::Type,
"int" => Self::Int,
"float" => Self::Float,
"complex" => Self::Complex,
"str" => Self::Str,
"set" => Self::Set,
"frozenset" => Self::FrozenSet,
"dict" => Self::Dict,
"list" => Self::List,
"slice" => Self::Slice,
"property" => Self::Property,
"BaseException" => Self::BaseException,
"BaseExceptionGroup" => Self::BaseExceptionGroup,
"Exception" => Self::Exception,
"ExceptionGroup" => Self::ExceptionGroup,
"staticmethod" => Self::Staticmethod,
"classmethod" => Self::Classmethod,
"Awaitable" => Self::Awaitable,
"Generator" => Self::Generator,
"deprecated" => Self::Deprecated,
"GenericAlias" => Self::GenericAlias,
"NoneType" => Self::NoneType,
"ModuleType" => Self::ModuleType,
"GeneratorType" => Self::GeneratorType,
"AsyncGeneratorType" => Self::AsyncGeneratorType,
"CoroutineType" => Self::CoroutineType,
"FunctionType" => Self::FunctionType,
"MethodType" => Self::MethodType,
"UnionType" => Self::UnionType,
"MethodWrapperType" => Self::MethodWrapperType,
"WrapperDescriptorType" => Self::WrapperDescriptorType,
"BuiltinFunctionType" => Self::BuiltinFunctionType,
"NewType" => Self::NewType,
"TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar,
"Iterable" => Self::Iterable,
"Iterator" => Self::Iterator,
"ParamSpec" => Self::ParamSpec,
"ParamSpecArgs" => Self::ParamSpecArgs,
"ParamSpecKwargs" => Self::ParamSpecKwargs,
"TypeVarTuple" => Self::TypeVarTuple,
"ChainMap" => Self::ChainMap,
"Counter" => Self::Counter,
"defaultdict" => Self::DefaultDict,
"deque" => Self::Deque,
"OrderedDict" => Self::OrderedDict,
"_Alias" => Self::StdlibAlias,
"_SpecialForm" => Self::SpecialForm,
"_NoDefaultType" => Self::NoDefaultType,
"SupportsIndex" => Self::SupportsIndex,
"Enum" => Self::Enum,
"EnumMeta" => Self::EnumType,
let candidates: &[Self] = match class_name {
"bool" => &[Self::Bool],
"object" => &[Self::Object],
"bytes" => &[Self::Bytes],
"bytearray" => &[Self::Bytearray],
"tuple" => &[Self::Tuple],
"type" => &[Self::Type],
"int" => &[Self::Int],
"float" => &[Self::Float],
"complex" => &[Self::Complex],
"str" => &[Self::Str],
"set" => &[Self::Set],
"frozenset" => &[Self::FrozenSet],
"dict" => &[Self::Dict],
"list" => &[Self::List],
"slice" => &[Self::Slice],
"property" => &[Self::Property],
"BaseException" => &[Self::BaseException],
"BaseExceptionGroup" => &[Self::BaseExceptionGroup],
"Exception" => &[Self::Exception],
"ExceptionGroup" => &[Self::ExceptionGroup],
"staticmethod" => &[Self::Staticmethod],
"classmethod" => &[Self::Classmethod],
"Awaitable" => &[Self::Awaitable],
"Generator" => &[Self::Generator],
"deprecated" => &[Self::Deprecated],
"GenericAlias" => &[Self::GenericAlias],
"NoneType" => &[Self::NoneType],
"ModuleType" => &[Self::ModuleType],
"GeneratorType" => &[Self::GeneratorType],
"AsyncGeneratorType" => &[Self::AsyncGeneratorType],
"CoroutineType" => &[Self::CoroutineType],
"FunctionType" => &[Self::FunctionType],
"MethodType" => &[Self::MethodType],
"UnionType" => &[Self::UnionType],
"MethodWrapperType" => &[Self::MethodWrapperType],
"WrapperDescriptorType" => &[Self::WrapperDescriptorType],
"BuiltinFunctionType" => &[Self::BuiltinFunctionType],
"NewType" => &[Self::NewType],
"TypeAliasType" => &[Self::TypeAliasType],
"TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar],
"Iterable" => &[Self::Iterable],
"Iterator" => &[Self::Iterator],
"ParamSpec" => &[Self::ParamSpec],
"ParamSpecArgs" => &[Self::ParamSpecArgs],
"ParamSpecKwargs" => &[Self::ParamSpecKwargs],
"TypeVarTuple" => &[Self::TypeVarTuple],
"ChainMap" => &[Self::ChainMap],
"Counter" => &[Self::Counter],
"defaultdict" => &[Self::DefaultDict],
"deque" => &[Self::Deque],
"OrderedDict" => &[Self::OrderedDict],
"_Alias" => &[Self::StdlibAlias],
"_SpecialForm" => &[Self::SpecialForm],
"_NoDefaultType" => &[Self::NoDefaultType],
"SupportsIndex" => &[Self::SupportsIndex],
"Enum" => &[Self::Enum],
"EnumMeta" => &[Self::EnumType],
"EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => {
Self::EnumType
&[Self::EnumType]
}
"StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => {
Self::StrEnum
&[Self::StrEnum]
}
"auto" => Self::Auto,
"member" => Self::Member,
"nonmember" => Self::Nonmember,
"ABCMeta" => Self::ABCMeta,
"super" => Self::Super,
"_version_info" => Self::VersionInfo,
"auto" => &[Self::Auto],
"member" => &[Self::Member],
"nonmember" => &[Self::Nonmember],
"ABCMeta" => &[Self::ABCMeta],
"super" => &[Self::Super],
"_version_info" => &[Self::VersionInfo],
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
Self::EllipsisType
&[Self::EllipsisType]
}
"EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => {
Self::EllipsisType
&[Self::EllipsisType]
}
"_NotImplementedType" => Self::NotImplementedType,
"Field" => Self::Field,
"KW_ONLY" => Self::KwOnly,
"InitVar" => Self::InitVar,
"NamedTupleFallback" => Self::NamedTupleFallback,
"NamedTupleLike" => Self::NamedTupleLike,
"ConstraintSet" => Self::ConstraintSet,
"TypedDictFallback" => Self::TypedDictFallback,
"Template" => Self::Template,
"Path" => Self::Path,
"_ProtocolMeta" => Self::ProtocolMeta,
"_NotImplementedType" => &[Self::NotImplementedType],
"Field" => &[Self::Field],
"KW_ONLY" => &[Self::KwOnly],
"InitVar" => &[Self::InitVar],
"NamedTupleFallback" => &[Self::NamedTupleFallback],
"NamedTupleLike" => &[Self::NamedTupleLike],
"ConstraintSet" => &[Self::ConstraintSet],
"TypedDictFallback" => &[Self::TypedDictFallback],
"Template" => &[Self::Template],
"Path" => &[Self::Path],
"_ProtocolMeta" => &[Self::ProtocolMeta],
_ => return None,
};
candidate
.check_module(db, file_to_module(db, file)?.known(db)?)
.then_some(candidate)
let module = file_to_module(db, file)?.known(db)?;
candidates
.iter()
.copied()
.find(|&candidate| candidate.check_module(db, module))
}
/// Return `true` if the module of `self` matches `module`
@ -5028,6 +5042,8 @@ impl KnownClass {
| Self::InitVar
| Self::NamedTupleFallback
| Self::TypedDictFallback
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::Awaitable
@ -5036,7 +5052,6 @@ impl KnownClass {
| Self::Path => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm
| Self::TypeVar
| Self::TypeAliasType
| Self::NoDefaultType
| Self::SupportsIndex
@ -5059,7 +5074,6 @@ impl KnownClass {
context: &InferContext<'db, '_>,
index: &SemanticIndex<'db>,
overload: &mut Binding<'db>,
call_arguments: &CallArguments<'_, 'db>,
call_expression: &ast::ExprCall,
) {
let db = context.db();
@ -5132,6 +5146,7 @@ impl KnownClass {
_ => {}
}
}
KnownClass::Deprecated => {
// Parsing something of the form:
//
@ -5158,153 +5173,6 @@ impl KnownClass {
DeprecatedInstance::new(db, message.into_string_literal()),
)));
}
KnownClass::TypeVar => {
let assigned_to = index
.try_expression(ast::ExprRef::from(call_expression))
.and_then(|expr| expr.assigned_to(db));
let Some(target) = assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to.node(module).targets.as_slice() {
[ast::Expr::Name(target)] => Some(target),
_ => None,
}
}) else {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A legacy `typing.TypeVar` must be immediately assigned to a variable",
);
}
return;
};
let [
Some(name_param),
constraints,
bound,
default,
contravariant,
covariant,
_infer_variance,
] = overload.parameter_types()
else {
return;
};
let covariant = covariant
.map(|ty| ty.bool(db))
.unwrap_or(Truthiness::AlwaysFalse);
let contravariant = contravariant
.map(|ty| ty.bool(db))
.unwrap_or(Truthiness::AlwaysFalse);
let variance = match (contravariant, covariant) {
(Truthiness::Ambiguous, _) => {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"The `contravariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value",
);
}
return;
}
(_, Truthiness::Ambiguous) => {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"The `covariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value",
);
}
return;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A legacy `typing.TypeVar` cannot be both \
covariant and contravariant",
);
}
return;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
TypeVarVariance::Contravariant
}
(Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => TypeVarVariance::Covariant,
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
TypeVarVariance::Invariant
}
};
let name_param = name_param.into_string_literal().map(|name| name.value(db));
if name_param.is_none_or(|name_param| name_param != target.id) {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(format_args!(
"The name of a legacy `typing.TypeVar`{} must match \
the name of the variable it is assigned to (`{}`)",
if let Some(name_param) = name_param {
format!(" (`{name_param}`)")
} else {
String::new()
},
target.id,
));
}
return;
}
let bound_or_constraint = match (bound, constraints) {
(Some(bound), None) => {
Some(TypeVarBoundOrConstraints::UpperBound(*bound).into())
}
(None, Some(_constraints)) => {
// We don't use UnionType::from_elements or UnionBuilder here,
// because we don't want to simplify the list of constraints like
// we do with the elements of an actual union type.
// TODO: Consider using a new `OneOfType` connective here instead,
// since that more accurately represents the actual semantics of
// typevar constraints.
let elements = UnionType::new(
db,
overload
.arguments_for_parameter(call_arguments, 1)
.map(|(_, ty)| ty)
.collect::<Box<_>>(),
);
Some(TypeVarBoundOrConstraints::Constraints(elements).into())
}
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
// constrained
(Some(_), Some(_)) => return,
(None, None) => None,
};
let containing_assignment = index.expect_single_definition(target);
overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar(
TypeVarInstance::new(
db,
&target.id,
Some(containing_assignment),
bound_or_constraint,
Some(variance),
default.map(Into::into),
TypeVarKind::Legacy,
),
)));
}
KnownClass::TypeAliasType => {
let assigned_to = index

View file

@ -126,6 +126,7 @@ pub(crate) fn typing_self<'db>(
Some(TypeVarVariance::Invariant),
None,
TypeVarKind::TypingSelf,
None,
);
bind_typevar(
@ -396,7 +397,7 @@ impl<'db> GenericContext<'db> {
typevar: TypeVarInstance<'db>,
) -> Option<BoundTypeVarInstance<'db>> {
self.variables(db)
.find(|self_bound_typevar| self_bound_typevar.typevar(db) == typevar)
.find(|self_bound_typevar| self_bound_typevar.typevar(db).is_identical_to(db, typevar))
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not

View file

@ -631,7 +631,7 @@ struct DefinitionInferenceExtra<'db> {
/// Is this a cycle-recovery inference result, and if so, what kind?
cycle_recovery: Option<CycleRecovery<'db>>,
/// The definitions that are deferred.
/// The definitions that have some deferred parts.
deferred: Box<[Definition<'db>]>,
/// The diagnostics for this region.

View file

@ -53,11 +53,12 @@ use crate::types::diagnostic::{
CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION,
DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE,
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE, INVALID_PARAMETER_DEFAULT,
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, report_bad_dunder_set_call,
INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NAMED_TUPLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, report_bad_dunder_set_call,
report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
@ -95,7 +96,7 @@ use crate::types::{
Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type,
TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind,
TypedDictType, UnionBuilder, UnionType, binding_type, todo_type,
TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, todo_type,
};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition};
@ -212,9 +213,9 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
/// The list should only contain one entry per declaration at most.
declarations: VecMap<Definition<'db>, TypeAndQualifiers<'db>>,
/// The definitions that are deferred.
/// The definitions with deferred sub-parts.
///
/// The list should only contain one entry per deferred.
/// The list should only contain one entry per definition.
deferred: VecSet<Definition<'db>>,
/// The returned types and their corresponding ranges of the region, if it is a function body.
@ -497,8 +498,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// Infer the deferred types for the definitions here to consider the end-of-scope
// semantics.
// Infer deferred types for all definitions.
for definition in std::mem::take(&mut self.deferred) {
self.extend_definition(infer_deferred_types(self.db(), definition));
}
@ -1245,6 +1245,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
DefinitionKind::TypeVar(typevar) => {
self.infer_typevar_deferred(typevar.node(self.module()));
}
DefinitionKind::Assignment(assignment) => {
self.infer_assignment_deferred(assignment.value(self.module()));
}
_ => {}
}
}
@ -2961,6 +2964,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
None,
default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy),
TypeVarKind::Pep695,
None,
)));
self.add_declaration_with_binding(
node.into(),
@ -3993,7 +3997,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
unpacked.expression_type(target)
}
TargetKind::Single => {
let value_ty = self.infer_standalone_expression(value, TypeContext::default());
let tcx = TypeContext::default();
let value_ty = if let Some(standalone_expression) = self.index.try_expression(value)
{
self.infer_standalone_expression_impl(value, standalone_expression, tcx)
} else if let ast::Expr::Call(call_expr) = value {
// If the RHS is not a standalone expression, this is a simple assignment
// (single target, no unpackings). That means it's a valid syntactic form
// for a legacy TypeVar creation; check for that.
let callable_type = self.infer_maybe_standalone_expression(
call_expr.func.as_ref(),
TypeContext::default(),
);
let typevar_class = callable_type
.into_class_literal()
.and_then(|cls| cls.known(self.db()))
.filter(|cls| {
matches!(cls, KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
});
let ty = if let Some(typevar_class) = typevar_class {
self.infer_legacy_typevar(target, call_expr, definition, typevar_class)
} else {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
};
self.store_expression_type(value, ty);
ty
} else {
self.infer_expression(value, tcx)
};
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
// at runtime, but is always considered `True` in type checking.
@ -4024,6 +4057,272 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_binding(target.into(), definition, target_ty);
}
fn infer_legacy_typevar(
&mut self,
target: &ast::Expr,
call_expr: &ast::ExprCall,
definition: Definition<'db>,
known_class: KnownClass,
) -> Type<'db> {
fn error<'db>(
context: &InferContext<'db, '_>,
message: impl std::fmt::Display,
node: impl Ranged,
) -> Type<'db> {
if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) {
builder.into_diagnostic(message);
}
// If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to
// just creating a regular instance of `typing.TypeVar`.
KnownClass::TypeVar.to_instance(context.db())
}
let db = self.db();
let arguments = &call_expr.arguments;
let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar;
let assume_all_features = self.in_stub() || is_typing_extensions;
let python_version = Program::get(db).python_version(db);
let have_features_from =
|version: PythonVersion| assume_all_features || python_version >= version;
let mut has_bound = false;
let mut default = None;
let mut covariant = false;
let mut contravariant = false;
let mut name_param_ty = None;
if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) {
return error(
&self.context,
"Starred arguments are not supported in `TypeVar` creation",
starred,
);
}
for kwarg in &arguments.keywords {
let Some(identifier) = kwarg.arg.as_ref() else {
return error(
&self.context,
"Starred arguments are not supported in `TypeVar` creation",
kwarg,
);
};
match identifier.id().as_str() {
"name" => {
// Duplicate keyword argument is a syntax error, so we don't have to check if
// `name_param_ty.is_some()` here.
if !arguments.args.is_empty() {
return error(
&self.context,
"The `name` parameter of `TypeVar` can only be provided once.",
kwarg,
);
}
name_param_ty =
Some(self.infer_expression(&kwarg.value, TypeContext::default()));
}
"bound" => has_bound = true,
"covariant" => {
match self
.infer_expression(&kwarg.value, TypeContext::default())
.bool(db)
{
Truthiness::AlwaysTrue => covariant = true,
Truthiness::AlwaysFalse => {}
Truthiness::Ambiguous => {
return error(
&self.context,
"The `covariant` parameter of `TypeVar` \
cannot have an ambiguous truthiness",
&kwarg.value,
);
}
}
}
"contravariant" => {
match self
.infer_expression(&kwarg.value, TypeContext::default())
.bool(db)
{
Truthiness::AlwaysTrue => contravariant = true,
Truthiness::AlwaysFalse => {}
Truthiness::Ambiguous => {
return error(
&self.context,
"The `contravariant` parameter of `TypeVar` \
cannot have an ambiguous truthiness",
&kwarg.value,
);
}
}
}
"default" => {
if !have_features_from(PythonVersion::PY313) {
// We don't return here; this error is informational since this will error
// at runtime, but the user's intent is plain, we may as well respect it.
error(
&self.context,
"The `default` parameter of `typing.TypeVar` was added in Python 3.13",
kwarg,
);
}
default = Some(TypeVarDefaultEvaluation::Lazy);
}
"infer_variance" => {
if !have_features_from(PythonVersion::PY312) {
// We don't return here; this error is informational since this will error
// at runtime, but the user's intent is plain, we may as well respect it.
error(
&self.context,
"The `infer_variance` parameter of `typing.TypeVar` was added in Python 3.12",
kwarg,
);
}
// TODO support `infer_variance` in legacy TypeVars
if self
.infer_expression(&kwarg.value, TypeContext::default())
.bool(db)
.is_ambiguous()
{
return error(
&self.context,
"The `infer_variance` parameter of `TypeVar` \
cannot have an ambiguous truthiness",
&kwarg.value,
);
}
}
name => {
// We don't return here; this error is informational since this will error
// at runtime, but it will likely cause fewer cascading errors if we just
// ignore the unknown keyword and still understand as much of the typevar as we
// can.
error(
&self.context,
format_args!("Unknown keyword argument `{name}` in `TypeVar` creation",),
kwarg,
);
self.infer_expression(&kwarg.value, TypeContext::default());
}
}
}
let variance = match (covariant, contravariant) {
(true, true) => {
return error(
&self.context,
"A `TypeVar` cannot be both covariant and contravariant",
call_expr,
);
}
(true, false) => TypeVarVariance::Covariant,
(false, true) => TypeVarVariance::Contravariant,
(false, false) => TypeVarVariance::Invariant,
};
let Some(name_param_ty) = name_param_ty.or_else(|| {
arguments
.find_positional(0)
.map(|arg| self.infer_expression(arg, TypeContext::default()))
}) else {
return error(
&self.context,
"The `name` parameter of `TypeVar` is required.",
call_expr,
);
};
let Some(name_param) = name_param_ty
.into_string_literal()
.map(|name| name.value(db))
else {
return error(
&self.context,
"The first argument to `TypeVar` must be a string literal.",
call_expr,
);
};
let ast::Expr::Name(ast::ExprName {
id: target_name, ..
}) = target
else {
return error(
&self.context,
"A `TypeVar` definition must be a simple variable assignment",
target,
);
};
if name_param != target_name {
return error(
&self.context,
format_args!(
"The name of a `TypeVar` (`{name_param}`) must match \
the name of the variable it is assigned to (`{target_name}`)"
),
target,
);
}
// Inference of bounds, constraints, and defaults must be deferred, to avoid cycles. So we
// only check presence/absence/number here.
let num_constraints = arguments.args.len().saturating_sub(1);
let bound_or_constraints = match (has_bound, num_constraints) {
(false, 0) => None,
(true, 0) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound),
(true, _) => {
return error(
&self.context,
"A `TypeVar` cannot have both a bound and constraints",
call_expr,
);
}
(_, 1) => {
return error(
&self.context,
"A `TypeVar` cannot have exactly one constraint",
&arguments.args[1],
);
}
(false, _) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints),
};
if bound_or_constraints.is_some() || default.is_some() {
self.deferred.insert(definition);
}
Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
db,
target_name,
Some(definition),
bound_or_constraints,
Some(variance),
default,
TypeVarKind::Legacy,
None,
)))
}
fn infer_assignment_deferred(&mut self, value: &ast::Expr) {
// Infer deferred bounds/constraints/defaults of a legacy TypeVar.
let ast::Expr::Call(ast::ExprCall { arguments, .. }) = value else {
return;
};
for arg in arguments.args.iter().skip(1) {
self.infer_type_expression(arg);
}
if let Some(bound) = arguments.find_keyword("bound") {
self.infer_type_expression(&bound.value);
}
if let Some(default) = arguments.find_keyword("default") {
self.infer_type_expression(&default.value);
}
}
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
if assignment.target.is_name_expr() {
self.infer_definition(assignment);
@ -6045,6 +6344,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&mut self,
call_expression: &ast::ExprCall,
tcx: TypeContext<'db>,
) -> Type<'db> {
// TODO: Use the type context for more precise inference.
let callable_type =
self.infer_maybe_standalone_expression(&call_expression.func, TypeContext::default());
self.infer_call_expression_impl(call_expression, callable_type, tcx)
}
fn infer_call_expression_impl(
&mut self,
call_expression: &ast::ExprCall,
callable_type: Type<'db>,
tcx: TypeContext<'db>,
) -> Type<'db> {
let ast::ExprCall {
range: _,
@ -6065,9 +6377,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ty
});
// TODO: Use the type context for more precise inference.
let callable_type = self.infer_maybe_standalone_expression(func, TypeContext::default());
// Special handling for `TypedDict` method calls
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
let value_type = self.expression_type(value);
@ -6171,7 +6480,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownClass::Object
| KnownClass::Property
| KnownClass::Super
| KnownClass::TypeVar
| KnownClass::TypeAliasType
| KnownClass::Deprecated
)
@ -6194,6 +6502,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
if matches!(
class.known(self.db()),
Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
) {
// Inference of correctly-placed `TypeVar` definitions is done in
// `TypeInferenceBuilder::infer_legacy_typevar`, and doesn't use the full
// call-binding machinery. If we reach here, it means that someone is trying to
// instantiate a `typing.TypeVar` in an invalid context.
if let Some(builder) = self
.context
.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A `TypeVar` definition must be a simple variable assignment",
);
}
}
return callable_type
.try_call_constructor(self.db(), call_arguments, tcx)
.unwrap_or_else(|err| {
@ -6253,7 +6579,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&self.context,
self.index,
overload,
&call_arguments,
call_expression,
);
}
@ -9353,7 +9678,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
assert!(
deferred.is_empty(),
"Expression region can't have deferred types"
"Expression region can't have deferred definitions"
);
let extra =

View file

@ -418,7 +418,8 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
"/src/main.py",
r#"
from mod import C
x = C().attr
# multiple targets ensures RHS is a standalone expression, relied on by this test
x = y = C().attr
"#,
)?;
@ -508,7 +509,8 @@ fn dependency_own_instance_member() -> anyhow::Result<()> {
"/src/main.py",
r#"
from mod import C
x = C().attr
# multiple targets ensures RHS is a standalone expression, relied on by this test
x = y = C().attr
"#,
)?;
@ -603,7 +605,8 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> {
r#"
from mod import C
C.method()
x = C().class_attr
# multiple targets ensures RHS is a standalone expression, relied on by this test
x = y = C().class_attr
"#,
)?;
@ -688,7 +691,8 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> {
r#"
from foo import foo
a = foo()
# multiple targets ensures RHS is a standalone expression, relied on by this test
a = b = foo()
"#,
)?;