Skip to content
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
3be8257
Build context for generic classes
dcreager Mar 24, 2025
4c76cb5
Handle explicit specialization before outputting lints
dcreager Mar 24, 2025
8048604
Explicitly specialize classes
dcreager Mar 27, 2025
fe76d56
Track those structs
dcreager Mar 27, 2025
4346e7d
Add Specialization
dcreager Mar 27, 2025
2380ade
Add CallableType::Specialized
dcreager Mar 30, 2025
178ee89
Use union to hold typevar constraints
dcreager Mar 31, 2025
feb1ea9
Add Type::TypeVar variant
dcreager Mar 31, 2025
3c1ad79
Fix failing tests
dcreager Mar 31, 2025
59430b6
doc Type::TypeVar
dcreager Apr 1, 2025
77ffa80
More correct handling of final bounds/constraints
dcreager Apr 1, 2025
6f86720
use list[T] so generic funcs are callable even with Never
dcreager Apr 1, 2025
cf81967
lint
dcreager Apr 1, 2025
6615df1
Add (currently failing) narrowing tests
dcreager Apr 1, 2025
b2f5a2a
Typevars _can_ be fully static I guess
dcreager Apr 1, 2025
590680c
Simplify intersections with constrained typevars
dcreager Apr 1, 2025
e6b7d40
Merge branch 'main' into dcreager/typevar-type
dcreager Apr 1, 2025
debd60a
Fix tests
dcreager Apr 1, 2025
aa391fd
lint
dcreager Apr 1, 2025
e57e62e
Update crates/red_knot_python_semantic/src/types/type_ordering.rs
dcreager Apr 1, 2025
3df79cc
Clarify that typevar is subtype of object too
dcreager Apr 2, 2025
5b08e93
Clarify non-fully-static bounded typevars aren't subtypes
dcreager Apr 2, 2025
82e810f
Add more tests for constrained gradual typevars
dcreager Apr 2, 2025
a3d7253
Update crates/red_knot_python_semantic/src/types.rs
dcreager Apr 2, 2025
15682d5
Simplify intersections with constrained typevars w/o glossing into union
dcreager Apr 2, 2025
9e07efe
Simplify positive intersections too
dcreager Apr 2, 2025
fb63c22
Intersection of constraints is subtype of typevar
dcreager Apr 2, 2025
3459056
Better descriptions of intersections of constrained typevars
dcreager Apr 2, 2025
71d425e
Add multiple narrowing example
dcreager Apr 2, 2025
233e938
lint
dcreager Apr 2, 2025
9785202
Sort typevar constraints
dcreager Apr 2, 2025
c86af50
Remove moot todo
dcreager Apr 2, 2025
aa00895
Fold typevar match arms back into main match statement
dcreager Apr 3, 2025
d99d1d7
Remove moot comment
dcreager Apr 3, 2025
58f0995
Remove moot todo
dcreager Apr 3, 2025
42fd54a
Add more TODOs about OneOf connector
dcreager Apr 3, 2025
6bd69f1
add todos for unary/binary ops
dcreager Apr 3, 2025
1d6a917
Merge remote-tracking branch 'origin/main' into dcreager/typevar-type
dcreager Apr 3, 2025
37692f1
Merge remote-tracking branch 'origin/dcreager/typevar-type' into dcre…
dcreager Apr 3, 2025
3869dc6
Fix tests
dcreager Apr 3, 2025
0c1745b
Fix tests better
dcreager Apr 3, 2025
8bdd9e2
Support unary and binary ops
dcreager Apr 3, 2025
39244dd
Merge remote-tracking branch 'origin/main' into dcreager/typevar-type
dcreager Apr 3, 2025
5694b3d
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 3, 2025
123b920
Fix merge conflicts
dcreager Apr 3, 2025
9e1767f
Rename to SpecializedCallable{,Type}
dcreager Apr 3, 2025
23ac0b6
Merge branch 'dcreager/typevar-type' into dcreager/special-class
dcreager Apr 3, 2025
6b27947
Specialize types
dcreager Apr 3, 2025
669aa21
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 3, 2025
575998e
Fix tests in ide crate
dcreager Apr 3, 2025
f02fefa
Add GenericClass, NonGenericClass, and GenericAlias
dcreager Mar 30, 2025
cc3a3df
Add GenericAlias
dcreager Apr 4, 2025
18af8b6
Apply specializations
dcreager Apr 4, 2025
af52fd1
Use class literal for self member lookups
dcreager Apr 4, 2025
d3fd822
Display specializations and generic aliases
dcreager Apr 4, 2025
aa64990
Specialize generic base class in generic subclass
dcreager Apr 4, 2025
e41f889
Merge branch 'main' into dcreager/special-class
dcreager Apr 4, 2025
7eb7c28
Only specialize function literals, not all callables
dcreager Apr 7, 2025
adb4aba
Fix descriptor protocol tests
dcreager Apr 7, 2025
968e637
Merge branch 'main' into dcreager/special-class
dcreager Apr 7, 2025
fd7914a
Remove unused methods
dcreager Apr 7, 2025
7ebda98
clippy
dcreager Apr 7, 2025
6257e89
Apply specialization to signature in-place
dcreager Apr 7, 2025
78cd92a
typo
dcreager Apr 7, 2025
530e2bc
Fix test case
dcreager Apr 7, 2025
637fd48
lint
dcreager Apr 7, 2025
ec5a588
Defer class definitions with string literals in base classes
dcreager Apr 7, 2025
27cb208
fix lint fix
dcreager Apr 7, 2025
ea12548
skip failing test for now
dcreager Apr 7, 2025
c376dad
clippy and lint
dcreager Apr 7, 2025
311dc59
add xfail for specializing to union of constraints
dcreager Apr 7, 2025
5fc8425
specialize property types
dcreager Apr 7, 2025
7aaeb47
Don't specialize function twice
dcreager Apr 7, 2025
43554e2
fix docs
dcreager Apr 7, 2025
0d656db
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 9, 2025
7c3405a
Better TODO fallback type
dcreager Apr 9, 2025
6593d90
Generic aliases are literals in type display
dcreager Apr 9, 2025
7ca6a60
Explain self_instance not being specialized
dcreager Apr 9, 2025
dea493e
Comment other non-specializations
dcreager Apr 9, 2025
048bb8b
Add xfail for generic method inside generic class
dcreager Apr 9, 2025
06859fa
More Python-like displays for specializations
dcreager Apr 9, 2025
d138405
Narrow type(generic) better
dcreager Apr 9, 2025
0997eca
Better todos
dcreager Apr 9, 2025
6ab1b90
Add TODO about property test data
dcreager Apr 9, 2025
bcb147f
lint
dcreager Apr 9, 2025
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 @@ -13,17 +13,13 @@ class C[T]: ...
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:

```py
# TODO: no error
# error: [non-subscriptable]
class D[U](C[U]): ...
```

A class that inherits from a generic class, but fills its type parameters with concrete types, is
_not_ generic:

```py
# TODO: no error
# error: [non-subscriptable]
class E(C[int]): ...
```

Expand Down Expand Up @@ -57,33 +53,85 @@ class D(C[T]): ...

(Examples `E` and `F` from above do not have analogues in the legacy syntax.)

## Inferring generic class parameters
## Specializing generic classes explicitly

The type parameter can be specified explicitly:

```py
class C[T]:
x: T

# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]()) # revealed: C
reveal_type(C[int]()) # revealed: C[int]
```

The specialization must match the generic types:

```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```

If the type variable has an upper bound, the specialized type must satisfy that bound:

```py
class Bounded[T: int]: ...
class BoundedByUnion[T: int | str]: ...
class IntSubclass(int): ...

reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]

# error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnostics look like this because I'm reusing the call binding mechanism to match parameters and check types. It would be nice for this diagnostic to talk about type parameters, but would prefer to do that as a follow-on.

reveal_type(Bounded[str]()) # revealed: Unknown

# error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
reveal_type(Bounded[int | str]()) # revealed: Unknown

reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
```

If the type variable is constrained, the specialized type must satisfy those constraints:

```py
class Constrained[T: (int, str)]: ...

reveal_type(Constrained[int]()) # revealed: Constrained[int]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]

reveal_type(Constrained[str]()) # revealed: Constrained[str]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]

# error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`"
reveal_type(Constrained[object]()) # revealed: Unknown
```

## Inferring generic class parameters

We can infer the type parameter from a type context:

```py
class C[T]:
x: T

c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C
reveal_type(c) # revealed: C[Unknown]
```

The typevars of a fully specialized generic class should no longer be visible:

```py
# TODO: revealed: int
reveal_type(c.x) # revealed: T
reveal_type(c.x) # revealed: Unknown
```

If the type parameter is not specified explicitly, and there are no constraints that let us infer a
Expand All @@ -92,15 +140,13 @@ specific type, we infer the typevar's default type:
```py
class D[T = int]: ...

# TODO: revealed: D[int]
reveal_type(D()) # revealed: D
reveal_type(D()) # revealed: D[int]
```

If a typevar does not provide a default, we use `Unknown`:

```py
# TODO: revealed: C[Unknown]
reveal_type(C()) # revealed: C
reveal_type(C()) # revealed: C[Unknown]
```

If the type of a constructor parameter is a class typevar, we can use that to infer the type
Expand All @@ -111,7 +157,7 @@ class E[T]:
def __init__(self, x: T) -> None: ...

# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1)) # revealed: E
reveal_type(E(1)) # revealed: E[Unknown]
```

The types inferred from a type context and from a constructor parameter must be consistent with each
Expand All @@ -131,17 +177,10 @@ propagate through:
class Base[T]:
x: T | None = None

# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...

# TODO: no error
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x) # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x) # revealed: T | None
reveal_type(Base[int].x) # revealed: int | None
reveal_type(Sub[int].x) # revealed: int | None
```

## Cyclic class definition
Expand All @@ -155,8 +194,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet

```pyi
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base[Sub]): ...

reveal_type(Sub) # revealed: Literal[Sub]
Expand All @@ -168,9 +205,6 @@ A similar case can work in a non-stub file, if forward references are stringifie

```py
class Base[T]: ...

# TODO: no error
# error: [non-subscriptable]
class Sub(Base["Sub"]): ...

reveal_type(Sub) # revealed: Literal[Sub]
Expand All @@ -183,8 +217,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE
```py
class Base[T]: ...

# TODO: the unresolved-reference error is correct, the non-subscriptable is not
# error: [non-subscriptable]
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,51 @@ class C[T]:
def m2(self, x: T) -> T:
return x

c: C[int] = C()
# TODO: no error
# error: [invalid-argument-type]
c: C[int] = C[int]()
c.m1(1)
# TODO: no error
# error: [invalid-argument-type]
c.m2(1)
# TODO: expected type `int`
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`"
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `int`"
c.m2("string")
```

## Functions on generic classes are descriptors

This repeats the tests in the [Functions as descriptors](./call/methods.md) test suite, but on a
generic class. This ensures that we are carrying any specializations through the entirety of the
descriptor protocol, which is how `self` parameters are bound to instance methods.

```py
from inspect import getattr_static

class C[T]:
def f(self, x: T) -> str:
return "a"

reveal_type(getattr_static(C[int], "f")) # revealed: Literal[<f specialized with {T = int}>]
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f` specialized with {T = int}>
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[<f specialized with {T = int}>]
# revealed: <bound method `f` of `C[int]` specialized with {T = int}>
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))

reveal_type(C[int].f) # revealed: Literal[<f specialized with {T = int}>]
reveal_type(C[int]().f) # revealed: <bound method `f` of `C[int]` specialized with {T = int}>

bound_method = C[int]().f
reveal_type(bound_method.__self__) # revealed: C[int]
reveal_type(bound_method.__func__) # revealed: Literal[<f specialized with {T = int}>]

reveal_type(C[int]().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str

C[int].f(1) # error: [missing-argument]
reveal_type(C[int].f(C[int](), 1)) # revealed: str

class D[U](C[U]):
pass

reveal_type(D[int]().f) # revealed: <bound method `f` of `D[int]` specialized with {T = int}>
```

## Methods can mention other typevars

> A type variable used in a method that does not match any of the variables that parameterize the
Expand Down Expand Up @@ -127,7 +160,6 @@ c: C[int] = C()
# TODO: no errors
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(c.m(1, "string")) # revealed: S
```

Expand Down
24 changes: 14 additions & 10 deletions crates/red_knot_python_semantic/resources/mdtest/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,14 @@ reveal_type(c.attr) # revealed: Unknown

## Behind the scenes

> TODO: This test is currently disabled pending
> [an upstream Salsa fix](https://github.com/salsa-rs/salsa/pull/741). Once that has been merged,
> re-enable this test by changing the language codes below back to `py`.
Comment on lines +199 to +201
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depends on salsa-rs/salsa#741

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to unblock this PR on the salsa fix, the other thing we can do is temporarily revert the change that makes mdtests reuse the same Salsa db, and live with slow mdtests until the Salsa fix lands.


In this section, we trace through some of the steps that make properties work. We start with a
simple class `C` and a property `attr`:

```py
```ignore
class C:
def __init__(self):
self._attr: int = 0
Expand All @@ -216,7 +220,7 @@ class C:
Next, we create an instance of `C`. As we have seen above, accessing `attr` on the instance will
return an `int`:

```py
```ignore
c = C()

reveal_type(c.attr) # revealed: int
Expand All @@ -226,7 +230,7 @@ Behind the scenes, when we write `c.attr`, the first thing that happens is that
up the symbol `attr` on the meta-type of `c`, i.e. the class `C`. We can emulate this static lookup
using `inspect.getattr_static`, to see that `attr` is actually an instance of the `property` class:

```py
```ignore
from inspect import getattr_static

attr_property = getattr_static(C, "attr")
Expand All @@ -237,7 +241,7 @@ The `property` class has a `__get__` method, which makes it a descriptor. It als
method, which means that it is a *data* descriptor (if there is no setter, `__set__` is still
available but yields an `AttributeError` at runtime).

```py
```ignore
reveal_type(type(attr_property).__get__) # revealed: <wrapper-descriptor `__get__` of `property` objects>
reveal_type(type(attr_property).__set__) # revealed: <wrapper-descriptor `__set__` of `property` objects>
```
Expand All @@ -246,22 +250,22 @@ When we access `c.attr`, the `__get__` method of the `property` class is called,
property object itself as the first argument, and the class instance `c` as the second argument. The
third argument is the "owner" which can be set to `None` or to `C` in this case:

```py
```ignore
reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int
reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int
```

Alternatively, the above can also be written as a method call:

```py
```ignore
reveal_type(attr_property.__get__(c, C)) # revealed: int
```

When we access `attr` on the class itself, the descriptor protocol is also invoked, but the instance
argument is set to `None`. When `instance` is `None`, the call to `property.__get__` returns the
property instance itself. So the following expressions are all equivalent

```py
```ignore
reveal_type(attr_property) # revealed: property
reveal_type(C.attr) # revealed: property
reveal_type(attr_property.__get__(None, C)) # revealed: property
Expand All @@ -271,7 +275,7 @@ reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: pr
When we set the property using `c.attr = "a"`, the `__set__` method of the property class is called.
This attribute access desugars to

```py
```ignore
type(attr_property).__set__(attr_property, c, "a")

# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed"
Expand All @@ -280,7 +284,7 @@ type(attr_property).__set__(attr_property, c, 1)

which is also equivalent to the following expressions:

```py
```ignore
attr_property.__set__(c, "a")
# error: [call-non-callable]
attr_property.__set__(c, 1)
Expand All @@ -293,7 +297,7 @@ C.attr.__set__(c, 1)
Properties also have `fget` and `fset` attributes that can be used to retrieve the original getter
and setter functions, respectively.

```py
```ignore
reveal_type(attr_property.fget) # revealed: Literal[attr]
reveal_type(attr_property.fget(c)) # revealed: int

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ In type stubs, classes can reference themselves in their base class definitions.
```pyi
class Foo[T]: ...

# TODO: actually is subscriptable
# error: [non-subscriptable]
class Bar(Foo[Bar]): ...

reveal_type(Bar) # revealed: Literal[Bar]
# TODO: Instead of `Literal[Foo]`, we might eventually want to show a type that involves the type parameter.
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Foo[Bar], Literal[object]]
```

## Access to attributes declared in stubs
Expand Down
11 changes: 11 additions & 0 deletions crates/red_knot_python_semantic/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,17 @@ impl<'db> SymbolAndQualifiers<'db> {
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
}

#[must_use]
pub(crate) fn map_type(
self,
f: impl FnOnce(Type<'db>) -> Type<'db>,
) -> SymbolAndQualifiers<'db> {
SymbolAndQualifiers {
symbol: self.symbol.map_type(f),
qualifiers: self.qualifiers,
}
}

/// Transform symbol and qualifiers into a [`LookupResult`],
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
Expand Down
Loading
Loading