[ty] Add support for PEP 800 (#20084)

This commit is contained in:
Alex Waygood 2025-08-25 19:39:05 +01:00 committed by GitHub
parent 33c5f6f4f8
commit ecf3c4ca11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 275 additions and 271 deletions

View file

@ -103,7 +103,7 @@ class E( # error: [instance-layout-conflict]
): ...
```
## A single "solid base"
## A single "disjoint base"
```py
class A:
@ -152,14 +152,15 @@ class Baz(Foo, Bar): ... # fine
<!-- snapshot-diagnostics -->
Certain classes implemented in C extensions also have an extended instance memory layout, in the
same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes
with a unique instance memory layout "solid bases", and we also borrow this term.) There is
currently no generalized way for ty to detect such a C-extension class, as there is currently no way
of expressing the fact that a class is a solid base in a stub file. However, ty special-cases
certain builtin classes in order to detect that attempting to combine them in a single MRO would
fail:
same way as classes that define non-empty `__slots__`. CPython internally calls all such classes
with a unique instance memory layout "solid bases", but [PEP 800](https://peps.python.org/pep-0800/)
calls these classes "disjoint bases", and this is the term we generally use. The `@disjoint_base`
decorator introduced by this PEP provides a generalised way for type checkers to identify such
classes.
```py
from typing_extensions import disjoint_base
# fmt: off
class A( # error: [instance-layout-conflict]
@ -183,6 +184,17 @@ class E( # error: [instance-layout-conflict]
class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
@disjoint_base
class G: ...
@disjoint_base
class H: ...
class I( # error: [instance-layout-conflict]
G,
H
): ...
# fmt: on
```
@ -193,9 +205,9 @@ We avoid emitting an `instance-layout-conflict` diagnostic for this class defini
class Foo(range, str): ... # error: [subclass-of-final-class]
```
## Multiple "solid bases" where one is a subclass of the other
## Multiple "disjoint bases" where one is a subclass of the other
A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the
A class is permitted to multiple-inherit from multiple disjoint bases if one is a subclass of the
other:
```py

View file

@ -12,59 +12,72 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict
## mdtest_snippet.py
```
1 | # fmt: off
1 | from typing_extensions import disjoint_base
2 |
3 | class A( # error: [instance-layout-conflict]
4 | int,
5 | str
6 | ): ...
7 |
8 | class B:
9 | __slots__ = ("b",)
10 |
11 | class C( # error: [instance-layout-conflict]
12 | int,
13 | B,
14 | ): ...
15 | class D(int): ...
16 |
17 | class E( # error: [instance-layout-conflict]
18 | D,
19 | str
20 | ): ...
21 |
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
3 | # fmt: off
4 |
5 | class A( # error: [instance-layout-conflict]
6 | int,
7 | str
8 | ): ...
9 |
10 | class B:
11 | __slots__ = ("b",)
12 |
13 | class C( # error: [instance-layout-conflict]
14 | int,
15 | B,
16 | ): ...
17 | class D(int): ...
18 |
19 | class E( # error: [instance-layout-conflict]
20 | D,
21 | str
22 | ): ...
23 |
24 | # fmt: on
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
25 |
26 | @disjoint_base
27 | class G: ...
28 |
29 | @disjoint_base
30 | class H: ...
31 |
32 | class I( # error: [instance-layout-conflict]
33 | G,
34 | H
35 | ): ...
36 |
37 | # fmt: on
38 | class Foo(range, str): ... # error: [subclass-of-final-class]
```
# Diagnostics
```
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
--> src/mdtest_snippet.py:3:7
|
1 | # fmt: off
2 |
3 | class A( # error: [instance-layout-conflict]
| _______^
4 | | int,
5 | | str
6 | | ): ...
| |_^ Bases `int` and `str` cannot be combined in multiple inheritance
7 |
8 | class B:
|
--> src/mdtest_snippet.py:5:7
|
3 | # fmt: off
4 |
5 | class A( # error: [instance-layout-conflict]
| _______^
6 | | int,
7 | | str
8 | | ): ...
| |_^ Bases `int` and `str` cannot be combined in multiple inheritance
9 |
10 | class B:
|
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
--> src/mdtest_snippet.py:4:5
--> src/mdtest_snippet.py:6:5
|
3 | class A( # error: [instance-layout-conflict]
4 | int,
5 | class A( # error: [instance-layout-conflict]
6 | int,
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
5 | str
7 | str
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
6 | ): ...
8 | ): ...
|
info: rule `instance-layout-conflict` is enabled by default
@ -72,28 +85,28 @@ info: rule `instance-layout-conflict` is enabled by default
```
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
--> src/mdtest_snippet.py:11:7
--> src/mdtest_snippet.py:13:7
|
9 | __slots__ = ("b",)
10 |
11 | class C( # error: [instance-layout-conflict]
11 | __slots__ = ("b",)
12 |
13 | class C( # error: [instance-layout-conflict]
| _______^
12 | | int,
13 | | B,
14 | | ): ...
14 | | int,
15 | | B,
16 | | ): ...
| |_^ Bases `int` and `B` cannot be combined in multiple inheritance
15 | class D(int): ...
17 | class D(int): ...
|
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
--> src/mdtest_snippet.py:12:5
--> src/mdtest_snippet.py:14:5
|
11 | class C( # error: [instance-layout-conflict]
12 | int,
13 | class C( # error: [instance-layout-conflict]
14 | int,
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
13 | B,
15 | B,
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
14 | ): ...
15 | class D(int): ...
16 | ): ...
17 | class D(int): ...
|
info: rule `instance-layout-conflict` is enabled by default
@ -101,31 +114,31 @@ info: rule `instance-layout-conflict` is enabled by default
```
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
--> src/mdtest_snippet.py:17:7
--> src/mdtest_snippet.py:19:7
|
15 | class D(int): ...
16 |
17 | class E( # error: [instance-layout-conflict]
17 | class D(int): ...
18 |
19 | class E( # error: [instance-layout-conflict]
| _______^
18 | | D,
19 | | str
20 | | ): ...
20 | | D,
21 | | str
22 | | ): ...
| |_^ Bases `D` and `str` cannot be combined in multiple inheritance
21 |
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
23 |
24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
--> src/mdtest_snippet.py:18:5
--> src/mdtest_snippet.py:20:5
|
17 | class E( # error: [instance-layout-conflict]
18 | D,
19 | class E( # error: [instance-layout-conflict]
20 | D,
| -
| |
| `D` instances have a distinct memory layout because `D` inherits from `int`
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
19 | str
21 | str
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
20 | ): ...
22 | ): ...
|
info: rule `instance-layout-conflict` is enabled by default
@ -133,28 +146,57 @@ info: rule `instance-layout-conflict` is enabled by default
```
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
--> src/mdtest_snippet.py:22:7
--> src/mdtest_snippet.py:24:7
|
20 | ): ...
21 |
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance
22 | ): ...
23 |
24 | # fmt: on
24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance
25 |
26 | @disjoint_base
|
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
--> src/mdtest_snippet.py:22:9
--> src/mdtest_snippet.py:24:9
|
20 | ): ...
21 |
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
22 | ): ...
23 |
24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
| --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension
| | | |
| | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension
| | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
23 |
24 | # fmt: on
25 |
26 | @disjoint_base
|
info: rule `instance-layout-conflict` is enabled by default
```
```
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
--> src/mdtest_snippet.py:32:7
|
30 | class H: ...
31 |
32 | class I( # error: [instance-layout-conflict]
| _______^
33 | | G,
34 | | H
35 | | ): ...
| |_^ Bases `G` and `H` cannot be combined in multiple inheritance
36 |
37 | # fmt: on
|
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
--> src/mdtest_snippet.py:33:5
|
32 | class I( # error: [instance-layout-conflict]
33 | G,
| - `G` instances have a distinct memory layout because of the way `G` is implemented in a C extension
34 | H
| - `H` instances have a distinct memory layout because of the way `H` is implemented in a C extension
35 | ): ...
|
info: rule `instance-layout-conflict` is enabled by default
@ -162,10 +204,10 @@ info: rule `instance-layout-conflict` is enabled by default
```
error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range`
--> src/mdtest_snippet.py:25:11
--> src/mdtest_snippet.py:38:11
|
24 | # fmt: on
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
37 | # fmt: on
38 | class Foo(range, str): ... # error: [subclass-of-final-class]
| ^^^^^
|
info: rule `subclass-of-final-class` is enabled by default

View file

@ -87,7 +87,7 @@ static_assert(is_disjoint_from(memoryview, Foo))
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
```
## "Solid base" builtin types
## "Disjoint base" builtin types
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin
classes *cannot* generally be used in multiple inheritance with other builtin types. This is because
@ -95,11 +95,14 @@ the CPython interpreter considers these classes "solid bases": due to the way th
in C, they have atypical instance memory layouts. No class can ever have more than one "solid base"
in its MRO.
It's not currently possible for ty to detect in a generalized way whether a class is a "solid base"
or not, but we special-case some commonly used builtin types:
[PEP 800](https://peps.python.org/pep-0800/) provides a generalised way for type checkers to know
whether a class has an atypical instance memory layout via the `@disjoint_base` decorator; we
generally use the term "disjoint base" for these classes.
```py
import asyncio
from typing import Any
from typing_extensions import disjoint_base
from ty_extensions import static_assert, is_disjoint_from
class Foo: ...
@ -114,12 +117,23 @@ static_assert(is_disjoint_from(list, dict[Any, Any]))
static_assert(is_disjoint_from(list[Foo], dict[Any, Any]))
static_assert(is_disjoint_from(list[Any], dict[Any, Any]))
static_assert(is_disjoint_from(type[list], type[dict]))
static_assert(is_disjoint_from(asyncio.Task, dict))
@disjoint_base
class A: ...
@disjoint_base
class B: ...
static_assert(is_disjoint_from(A, B))
```
## Other solid bases
## Other disjoint bases
As well as certain classes that are implemented in C extensions, any class that declares non-empty
`__slots__` is also considered a "solid base"; these types are also considered to be disjoint by ty:
`__slots__` is also considered a "disjoint base"; these types are also considered to be disjoint by
ty:
```py
from ty_extensions import static_assert, is_disjoint_from
@ -141,7 +155,7 @@ static_assert(not is_disjoint_from(B, C))
static_assert(not is_disjoint_from(type[B], type[C]))
```
Two solid bases are not disjoint if one inherits from the other, however:
Two disjoint bases are not disjoint if one inherits from the other, however:
```py
class D(A):