ruff/crates/ty_python_semantic/resources/mdtest/stubs/class.md
Alex Waygood d02c9ada5d
[ty] Do not carry the generic context of Protocol or Generic in the ClassBase enum (#17989)
## Summary

It doesn't seem to be necessary for our generics implementation to carry
the `GenericContext` in the `ClassBase` variants. Removing it simplifies
the code, fixes many TODOs about `Generic` or `Protocol` appearing
multiple times in MROs when each should only appear at most once, and
allows us to more accurately detect runtime errors that occur due to
`Generic` or `Protocol` appearing multiple times in a class's bases.

In order to remove the `GenericContext` from the `ClassBase` variant, it
turns out to be necessary to emulate
`typing._GenericAlias.__mro_entries__`, or we end up with a large number
of false-positive `inconsistent-mro` errors. This PR therefore also does
that.

Lastly, this PR fixes the inferred MROs of PEP-695 generic classes,
which implicitly inherit from `Generic` even if they have no explicit
bases.

## Test Plan

mdtests
2025-05-22 21:37:03 -04:00

1.2 KiB

Class definitions in stubs

Cyclical class definition

[environment]
python-version = "3.12"

In type stubs, classes can reference themselves in their base class definitions. For example, in typeshed, we have class str(Sequence[str]): ....

class Foo[T]: ...

class Bar(Foo[Bar]): ...

reveal_type(Bar)  # revealed: <class 'Bar'>
reveal_type(Bar.__mro__)  # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, typing.Generic, <class 'object'>]

Access to attributes declared in stubs

Unlike regular Python modules, stub files often omit the right-hand side in declarations, including in class scope. However, from the perspective of the type checker, we have to treat them as bindings too. That is, symbol: type is the same as symbol: type = ....

One implication of this is that we'll always treat symbols in class scope as safe to be accessed from the class object itself. We'll never infer a "pure instance attribute" from a stub.

b.pyi:

from typing import ClassVar

class C:
    class_or_instance_var: int
from typing import ClassVar, Literal

from b import C

# No error here, since we treat `class_or_instance_var` as bound on the class.
reveal_type(C.class_or_instance_var)  # revealed: int