mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-03 13:23:10 +00:00
[ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
d89f75f9cc
commit
9d8cba4e8b
23 changed files with 1255 additions and 442 deletions
|
|
@ -192,16 +192,18 @@ def _(
|
|||
from typing import Callable, Union
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], Foo],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, Foo]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & Foo
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & Foo)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
|
|
|||
|
|
@ -88,3 +88,26 @@ def assigns_complex(x: complex):
|
|||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
`int`, `float` and `complex` are all disjoint, which means that the union `int | float` can easily
|
||||
be narrowed to `int` or `float`:
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
from ty_extensions import JustFloat
|
||||
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, float):
|
||||
reveal_type(x) # revealed: float
|
||||
else:
|
||||
reveal_type(x) # revealed: complex
|
||||
|
||||
assert isinstance(x, float)
|
||||
assert_type(x, JustFloat)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@ def _(target: int | None | float):
|
|||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
class Foo: ...
|
||||
|
||||
def _(target: None | Foo):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
|
|
|
|||
|
|
@ -653,7 +653,7 @@ from ty_extensions import Not
|
|||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# `__slots__`
|
||||
# Tests for ty's `instance-layout-conflict` error code
|
||||
|
||||
## Not specified and empty
|
||||
## `__slots__`: not specified or empty
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
|
@ -17,7 +17,9 @@ class BC(B, C): ... # fine
|
|||
class ABC(A, B, C): ... # fine
|
||||
```
|
||||
|
||||
## Incompatible tuples
|
||||
## `__slots__`: incompatible tuples
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -26,13 +28,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Same value
|
||||
## `__slots__` are the same value
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -41,13 +43,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Strings
|
||||
## `__slots__` is a string
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -56,13 +58,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("abc",)
|
||||
|
||||
class AB(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class AB( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Invalid
|
||||
## Invalid `__slots__` definitions
|
||||
|
||||
TODO: Emit diagnostics
|
||||
|
||||
|
|
@ -83,7 +85,7 @@ class NonIdentifier3:
|
|||
__slots__ = (e for e in ("lorem", "42"))
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
## Inherited `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -95,13 +97,13 @@ class C:
|
|||
__slots__ = ("c", "d")
|
||||
|
||||
class D(C): ...
|
||||
class E(
|
||||
B, # error: [incompatible-slots]
|
||||
D, # error: [incompatible-slots]
|
||||
class E( # error: [instance-layout-conflict]
|
||||
B,
|
||||
D,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Single solid base
|
||||
## A single "solid base"
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -113,7 +115,7 @@ class D(B, A): ... # fine
|
|||
class E(B, C, A): ... # fine
|
||||
```
|
||||
|
||||
## Post-hoc modifications
|
||||
## Post-hoc modifications to `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -125,15 +127,105 @@ reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
|
|||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Explicitly annotated `__slots__`
|
||||
|
||||
We do not emit false positives on classes with empty `__slots__` definitions, even if the
|
||||
`__slots__` definitions are annotated with variadic tuples:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Bar:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Baz(Foo, Bar): ... # fine
|
||||
```
|
||||
|
||||
## Built-ins with implicit layouts
|
||||
|
||||
<!-- 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:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
class A( # error: [instance-layout-conflict]
|
||||
int,
|
||||
str
|
||||
): ...
|
||||
|
||||
class B:
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C( # error: [instance-layout-conflict]
|
||||
int,
|
||||
B,
|
||||
): ...
|
||||
class D(int): ...
|
||||
|
||||
class E( # error: [instance-layout-conflict]
|
||||
D,
|
||||
str
|
||||
): ...
|
||||
|
||||
class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
We avoid emitting an `instance-layout-conflict` diagnostic for this class definition, because
|
||||
`range` is `@final`, so we'll complain about the `class` statement anyway:
|
||||
|
||||
```py
|
||||
class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
## Multiple "solid 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
|
||||
other:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B(A):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C(B, A): ... # fine
|
||||
```
|
||||
|
||||
The same principle, but a more complex example:
|
||||
|
||||
```py
|
||||
class AA:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class BB(AA):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class CC(BB): ...
|
||||
class DD(AA): ...
|
||||
class FF(CC, DD): ... # fine
|
||||
```
|
||||
|
||||
## False negatives
|
||||
|
||||
### Possibly unbound
|
||||
### Possibly unbound `__slots__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
|
|
@ -148,7 +240,7 @@ def _(flag: bool):
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Bound but with different types
|
||||
### Bound `__slots__` but with different types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
|
|
@ -165,7 +257,7 @@ def _(flag: bool):
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Non-tuples
|
||||
### Non-tuple `__slots__` definitions
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
|
@ -178,13 +270,6 @@ class B:
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Built-ins with implicit layouts
|
||||
|
||||
```py
|
||||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
|
||||
### Diagnostic if `__slots__` is externally modified
|
||||
|
||||
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
|
||||
|
|
@ -46,12 +46,15 @@ def _(flag1: bool, flag2: bool):
|
|||
## Assignment expressions
|
||||
|
||||
```py
|
||||
def f() -> int | str | None: ...
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
if isinstance(x := f(), int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, str):
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
def f() -> Foo | Bar | None: ...
|
||||
|
||||
if isinstance(x := f(), Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
elif isinstance(x, Bar):
|
||||
reveal_type(x) # revealed: Bar & ~Foo
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ match x:
|
|||
case 6.0:
|
||||
reveal_type(x) # revealed: float
|
||||
case 1j:
|
||||
reveal_type(x) # revealed: complex & ~float
|
||||
reveal_type(x) # revealed: complex
|
||||
case b"foo":
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ match x:
|
|||
case True | False:
|
||||
reveal_type(x) # revealed: bool
|
||||
case 3.14 | 2.718 | 1.414:
|
||||
reveal_type(x) # revealed: float & ~tuple[Unknown, ...]
|
||||
reveal_type(x) # revealed: float
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
|
|
|||
|
|
@ -178,25 +178,26 @@ def _(d: Any):
|
|||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_str(a: object) -> TypeGuard[str]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
if guard_str(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def _(a: Foo | Bar):
|
||||
if guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if is_int(a):
|
||||
reveal_type(a) # revealed: int
|
||||
if is_bar(a):
|
||||
reveal_type(a) # revealed: Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str & ~int
|
||||
reveal_type(a) # revealed: Foo & ~Bar
|
||||
```
|
||||
|
||||
Attribute and subscript narrowing is supported:
|
||||
|
|
@ -209,68 +210,68 @@ T = TypeVar("T")
|
|||
class C(Generic[T]):
|
||||
v: T
|
||||
|
||||
def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[str @ a[1]]`
|
||||
if reveal_type(guard_str(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a[1]) # revealed: int | str
|
||||
def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[Foo @ a[1]]`
|
||||
if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[Bar, Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a[1]) # revealed: Bar | Foo
|
||||
|
||||
if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
reveal_type(a[0]) # revealed: int
|
||||
if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]]
|
||||
# TODO: Should be `tuple[Bar, Bar & Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
reveal_type(a[0]) # revealed: Bar
|
||||
|
||||
# TODO: Should be `TypeGuard[str @ c.v]`
|
||||
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `TypeGuard[Foo @ c.v]`
|
||||
if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `str`
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
|
||||
if reveal_type(is_int(c.v)): # revealed: TypeIs[int @ c.v]
|
||||
if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
reveal_type(c.v) # revealed: Any & int
|
||||
reveal_type(c.v) # revealed: Any & Bar
|
||||
```
|
||||
|
||||
Indirect usage is supported within the same scope:
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
b = guard_str(a)
|
||||
c = is_int(a)
|
||||
def _(a: Foo | Bar):
|
||||
b = guard_foo(a)
|
||||
c = is_bar(a)
|
||||
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
# TODO: Should be `TypeGuard[Foo @ a]`
|
||||
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: TypeIs[int @ a]
|
||||
reveal_type(c) # revealed: TypeIs[Bar @ a]
|
||||
|
||||
if b:
|
||||
# TODO should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if c:
|
||||
# TODO should be `int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
# TODO should be `str & ~int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo & ~Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
Further writes to the narrowed place invalidate the narrowing:
|
||||
|
||||
```py
|
||||
def _(x: str | int, flag: bool) -> None:
|
||||
b = is_int(x)
|
||||
reveal_type(b) # revealed: TypeIs[int @ x]
|
||||
def _(x: Foo | Bar, flag: bool) -> None:
|
||||
b = is_bar(x)
|
||||
reveal_type(b) # revealed: TypeIs[Bar @ x]
|
||||
|
||||
if flag:
|
||||
x = ""
|
||||
x = Foo()
|
||||
|
||||
if b:
|
||||
reveal_type(x) # revealed: str | int
|
||||
reveal_type(x) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
The `TypeIs` type remains effective across generic boundaries:
|
||||
|
|
@ -280,19 +281,19 @@ from typing_extensions import TypeVar, reveal_type
|
|||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(v: object) -> TypeIs[int]:
|
||||
def f(v: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def g(v: T) -> T:
|
||||
return v
|
||||
|
||||
def _(a: str):
|
||||
def _(a: Foo):
|
||||
# `reveal_type()` has the type `[T]() -> T`
|
||||
if reveal_type(f(a)): # revealed: TypeIs[int @ a]
|
||||
reveal_type(a) # revealed: str & int
|
||||
if reveal_type(f(a)): # revealed: TypeIs[Bar @ a]
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
|
||||
if g(f(a)):
|
||||
reveal_type(a) # revealed: str & int
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
```
|
||||
|
||||
## `TypeGuard` special cases
|
||||
|
|
@ -301,28 +302,32 @@ def _(a: str):
|
|||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_int(a: object) -> TypeGuard[int]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
class Baz(Bar): ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def does_not_narrow_in_negative_case(a: str | int):
|
||||
if not guard_int(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def does_not_narrow_in_negative_case(a: Foo | Bar):
|
||||
if not guard_foo(a):
|
||||
# TODO: Should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
def narrowed_type_must_be_exact(a: object, b: bool):
|
||||
if guard_int(b):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(b) # revealed: bool
|
||||
def narrowed_type_must_be_exact(a: object, b: Baz):
|
||||
if guard_foo(b):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(b) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and is_int(a):
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Baz) and is_bar(a):
|
||||
reveal_type(a) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and guard_int(a):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Bar) and guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Bar
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Built-ins with implicit layouts
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | # fmt: off
|
||||
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]
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
25 | 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:
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
4 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
5 | str
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
6 | ): ...
|
||||
|
|
||||
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
|
||||
|
|
||||
9 | __slots__ = ("b",)
|
||||
10 |
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
12 | | int,
|
||||
13 | | B,
|
||||
14 | | ): ...
|
||||
| |_^ Bases `int` and `B` cannot be combined in multiple inheritance
|
||||
15 | 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
|
||||
|
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
12 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
13 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
14 | ): ...
|
||||
15 | class D(int): ...
|
||||
|
|
||||
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
|
||||
|
|
||||
15 | class D(int): ...
|
||||
16 |
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
18 | | D,
|
||||
19 | | str
|
||||
20 | | ): ...
|
||||
| |_^ Bases `D` and `str` cannot be combined in multiple inheritance
|
||||
21 |
|
||||
22 | 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
|
||||
|
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
18 | 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
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
20 | ): ...
|
||||
|
|
||||
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
|
||||
|
|
||||
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
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | 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
|
||||
|
|
||||
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
|
||||
|
|
||||
24 | # fmt: on
|
||||
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `subclass-of-final-class` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - `__slots__`: incompatible tuples
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | __slots__ = ("a", "b")
|
||||
3 |
|
||||
4 | class B:
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
9 | B,
|
||||
10 | ): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:7:7
|
||||
|
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
8 | | A,
|
||||
9 | | B,
|
||||
10 | | ): ...
|
||||
| |_^ Bases `A` and `B` cannot be combined in multiple inheritance
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
| - `A` instances have a distinct memory layout because `A` defines non-empty `__slots__`
|
||||
9 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
10 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -20,8 +20,6 @@ static_assert(not is_disjoint_from(Any, Not[Any]))
|
|||
|
||||
static_assert(not is_disjoint_from(LiteralString, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, type))
|
||||
static_assert(not is_disjoint_from(str, type[Any]))
|
||||
```
|
||||
|
||||
## Class hierarchies
|
||||
|
|
@ -71,6 +69,88 @@ class UsesMeta2(metaclass=Meta2): ...
|
|||
static_assert(is_disjoint_from(UsesMeta1, UsesMeta2))
|
||||
```
|
||||
|
||||
## `@final` builtin types
|
||||
|
||||
Some builtins types are declared as `@final`:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
# `range`, `slice` and `memoryview` are all declared as `@final`:
|
||||
static_assert(is_disjoint_from(range, Foo))
|
||||
static_assert(is_disjoint_from(type[range], type[Foo]))
|
||||
static_assert(is_disjoint_from(slice, Foo))
|
||||
static_assert(is_disjoint_from(type[slice], type[Foo]))
|
||||
static_assert(is_disjoint_from(memoryview, Foo))
|
||||
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
|
||||
```
|
||||
|
||||
## "Solid 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
|
||||
the CPython interpreter considers these classes "solid bases": due to the way they are implemented
|
||||
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:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
static_assert(is_disjoint_from(list, dict))
|
||||
static_assert(is_disjoint_from(list[Foo], dict))
|
||||
static_assert(is_disjoint_from(list[Any], dict))
|
||||
static_assert(is_disjoint_from(list, dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Foo], dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Any], dict[Foo, Foo]))
|
||||
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]))
|
||||
```
|
||||
|
||||
## Other solid 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:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class C:
|
||||
__slots__ = ()
|
||||
|
||||
static_assert(is_disjoint_from(A, B))
|
||||
static_assert(is_disjoint_from(type[A], type[B]))
|
||||
static_assert(not is_disjoint_from(A, C))
|
||||
static_assert(not is_disjoint_from(type[A], type[C]))
|
||||
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:
|
||||
|
||||
```py
|
||||
class D(A):
|
||||
__slots__ = ("d",)
|
||||
|
||||
static_assert(is_disjoint_from(D, B))
|
||||
static_assert(not is_disjoint_from(D, A))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
|
|
@ -396,8 +476,10 @@ reveal_type(C.prop) # revealed: property
|
|||
class D:
|
||||
pass
|
||||
|
||||
static_assert(not is_disjoint_from(int, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], int))
|
||||
class Whatever: ...
|
||||
|
||||
static_assert(not is_disjoint_from(Whatever, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], Whatever))
|
||||
static_assert(is_disjoint_from(TypeOf[C.prop], D))
|
||||
static_assert(is_disjoint_from(D, TypeOf[C.prop]))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -263,8 +263,10 @@ reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
|||
# revealed: Never
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
|
||||
# revealed: int & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str]]))
|
||||
class Foo: ...
|
||||
|
||||
# revealed: Foo & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]]))
|
||||
|
||||
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue