Skip to content

Commit

Permalink
[red-knot] Several failing tests for generics (#16509)
Browse files Browse the repository at this point in the history
To kick off the work of supporting generics, this adds many new
(currently failing) tests, showing the behavior we plan to support.

This is still missing a lot!  Not included:

- typevar tuples
- param specs
- variance
- `Self`

But it's a good start! We can add more failing tests for those once we
tackle these.

---------

Co-authored-by: Carl Meyer <[email protected]>
  • Loading branch information
dcreager and carljm authored Mar 5, 2025
1 parent 114abc7 commit ebd172e
Show file tree
Hide file tree
Showing 6 changed files with 808 additions and 81 deletions.
81 changes: 0 additions & 81 deletions crates/red_knot_python_semantic/resources/mdtest/generics.md

This file was deleted.

186 changes: 186 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/generics/classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Generic classes

## PEP 695 syntax

TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.

This is a generic class defined using PEP 695 syntax:

```py
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]): ...
```

A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly
uses the default value for the typevar. In this case, that default type is `Unknown`, so `F`
inherits from `C[Unknown]` and is not itself generic.

```py
class F(C): ...
```

## Legacy syntax

This is a generic class defined using the legacy syntax:

```py
from typing import Generic, TypeVar

T = TypeVar("T")

# TODO: no error
# error: [invalid-base]
class C(Generic[T]): ...
```

A class that inherits from a generic class, and fills its type parameters with typevars, is generic.

```py
class D(C[T]): ...
```

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

## Inferring generic class parameters

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: Unknown
```

We can infer the type parameter from a type context:

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

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

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

If the type parameter is not specified explicitly, and there are no constraints that let us infer a
specific type, we infer the typevar's default type:

```py
class D[T = int]: ...

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

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

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

If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter:

```py
class E[T]:
def __init__(self, x: T) -> None: ...

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

The types inferred from a type context and from a constructor parameter must be consistent with each
other:

```py
# TODO: error
wrong_innards: E[int] = E("five")
```

## Generic subclass

When a generic subclass fills its superclass's type parameter with one of its own, the actual types
propagate through:

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

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

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

## Cyclic class definition

A class can use itself as the type parameter of one of its superclasses. (This is also known as the
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)

Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).

`stub.pyi`:

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

reveal_type(Sub) # revealed: Literal[Sub]
```

`string_annotation.py`:

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

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

reveal_type(Sub) # revealed: Literal[Sub]
```

`bare_annotation.py`:

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

# TODO: error: [unresolved-reference]
class Sub(Base[Sub]): ...
```

[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
Loading

0 comments on commit ebd172e

Please sign in to comment.