Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int

def variable(x: int):
reveal_type(x**2) # revealed: int
reveal_type(x**2) # revealed: (int & Any) | (int & float & Any)
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
reveal_type(2**x) # revealed: (int & Any) | (int & float & Any)
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
reveal_type(x**x) # revealed: (int & Any) | (int & float & Any)
```

If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ class ConvertToLength:
class C:
converter: ConvertToLength = ConvertToLength()

reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
reveal_type(C.__init__) # revealed: (converter: str = Never) -> None

c = C("abc")
reveal_type(c.converter) # revealed: int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ class Descriptor:
class C:
d: Descriptor = Descriptor()

reveal_type(C.d) # revealed: Literal["called on class object"]
reveal_type(C.d) # revealed: Never

reveal_type(C().d) # revealed: Literal["called on instance"]
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ from typing import TypeVar

T = TypeVar("T")

# TODO: `invalid-return-type` error should be emitted
# error: [invalid-return-type]
def m(x: T) -> T: ...
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ in newer Python releases.
from typing import TypeVar

T = TypeVar("T")
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```

### Directly assigned to a variable
Expand All @@ -29,7 +32,12 @@ T = TypeVar("T")
```py
from typing import TypeVar

# TODO: error
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"
TestList = list[TypeVar("W")]
```

Expand All @@ -40,7 +48,7 @@ TestList = list[TypeVar("W")]
```py
from typing import TypeVar

# TODO: error
# 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`)"
T = TypeVar("Q")
```

Expand All @@ -57,6 +65,52 @@ T = TypeVar("T")
T = TypeVar("T")
```

### Type variables with a default

Note that the `__default__` property is only available in Python ≥3.13.

```toml
[environment]
python-version = "3.13"
```

```py
from typing import TypeVar

T = TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]

S = TypeVar("S")
reveal_type(S.__default__) # revealed: NoDefault
```

### Type variables with an upper bound

```py
from typing import TypeVar

T = TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]

S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
```

### Type variables with constraints

```py
from typing import TypeVar

T = TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]

S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
```

### Cannot have only one constraint

> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,51 @@ instances of `typing.TypeVar`, just like legacy type variables.
```py
def f[T]():
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: T
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```

### Type variables with a default

Note that the `__default__` property is only available in Python ≥3.13.

```toml
[environment]
python-version = "3.13"
```

```py
def f[T = int]():
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]

def g[S]():
reveal_type(S.__default__) # revealed: NoDefault
```

### Type variables with an upper bound

```py
def f[T: int]():
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]

def g[S]():
reveal_type(S.__bound__) # revealed: None
```

### Type variables with constraints

```py
def f[T: (int, str)]():
reveal_type(T.__constraints__) # revealed: tuple[int, str]
reveal_type(T.__bound__) # revealed: None

def g[S]():
reveal_type(S.__constraints__) # revealed: tuple[()]
```

### Cannot have only one constraint

> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ class Legacy(Generic[T]):
return y

legacy: Legacy[int] = Legacy()
# TODO: revealed: str
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
```

With PEP 695 syntax, it is clearer that the method uses a separate typevar:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
14 |
15 | T = TypeVar("T")
16 |
17 | # TODO: `invalid-return-type` error should be emitted
17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ...
```

Expand Down Expand Up @@ -79,3 +79,14 @@ error: lint:invalid-return-type: Return type does not match returned value
|

```

```
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `T`
--> src/mdtest_snippet.py:18:16
|
17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ...
| ^
|

```
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ x = [1, 2, 3]
reveal_type(x) # revealed: list

# TODO reveal int
reveal_type(x[0]) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
reveal_type(x[0]) # revealed: Unknown

# TODO reveal list
reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,8 @@ from functools import partial

def f(x: int, y: str) -> None: ...

# TODO: no error
# error: [invalid-assignment] "Object of type `partial` is not assignable to `(int, /) -> None`"
c1: Callable[[int], None] = partial(f, y="a")
```

Expand Down
25 changes: 22 additions & 3 deletions crates/red_knot_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,19 +754,35 @@ impl<'db> SemanticIndexBuilder<'db> {
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
/// standalone (type narrowing tests, RHS of an assignment.)
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal)
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal, None)
}

/// Record an expression that is immediately assigned to a target, and that needs to be a Salsa
/// ingredient, because we need to infer its type standalone (type narrowing tests, RHS of an
/// assignment.)
fn add_standalone_assigned_expression(
&mut self,
expression_node: &ast::Expr,
assigned_to: &ast::StmtAssign,
) -> Expression<'db> {
self.add_standalone_expression_impl(
expression_node,
ExpressionKind::Normal,
Some(assigned_to),
)
}

/// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a
/// *type* expression, which makes sure that it will later be inferred as such.
fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression)
self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression, None)
}

fn add_standalone_expression_impl(
&mut self,
expression_node: &ast::Expr,
expression_kind: ExpressionKind,
assigned_to: Option<&ast::StmtAssign>,
) -> Expression<'db> {
let expression = Expression::new(
self.db,
Expand All @@ -776,6 +792,9 @@ impl<'db> SemanticIndexBuilder<'db> {
unsafe {
AstNodeRef::new(self.module.clone(), expression_node)
},
#[allow(unsafe_code)]
assigned_to
.map(|assigned_to| unsafe { AstNodeRef::new(self.module.clone(), assigned_to) }),
expression_kind,
countme::Count::default(),
);
Expand Down Expand Up @@ -1377,7 +1396,7 @@ where
debug_assert_eq!(&self.current_assignments, &[]);

self.visit_expr(&node.value);
let value = self.add_standalone_expression(&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);
Expand Down
11 changes: 11 additions & 0 deletions crates/red_knot_python_semantic/src/semantic_index/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ pub(crate) struct Expression<'db> {
#[return_ref]
pub(crate) node_ref: AstNodeRef<ast::Expr>,

/// An assignment statement, if this expression is immediately used as the rhs of that
/// assignment.
///
/// (Note that this is the _immediately_ containing assignment — if a complex expression is
/// assigned to some target, only the outermost expression node has this set. The inner
/// expressions are used to build up the assignment result, and are not "immediately assigned"
/// to the target, and so have `None` for this field.)
#[no_eq]
#[tracked]
pub(crate) assigned_to: Option<AstNodeRef<ast::StmtAssign>>,

/// Should this expression be inferred as a normal expression or a type expression?
pub(crate) kind: ExpressionKind,

Expand Down
Loading
Loading