mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Add support for PEP 800 (#20084)
This commit is contained in:
parent
33c5f6f4f8
commit
ecf3c4ca11
9 changed files with 275 additions and 271 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue