From 9725a2d4768fc71b17bbbcaf59ed32a7c7a93644 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 20 Jan 2025 09:47:51 +0100 Subject: [PATCH] [red-knot] More exhaustive disjointness tests (#15606) ## Summary Mostly just brings the structure/format of the disjointness-tests closer to what we have for `is_subtype_of` etc. --- .../type_properties/is_disjoint_from.md | 273 ++++++++++++------ 1 file changed, 189 insertions(+), 84 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 87bc9b3dfc..2535d84bfb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -1,85 +1,60 @@ -# Tests for disjointness +# Disjointness relation -If two types can be disjoint, it means that it is known that no possible runtime object could ever -inhabit both types simultaneously. +Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`). +This means that it is known that no possible runtime object inhabits both types simultaneously. -## `Never` - -`Never` is disjoint from every type, including itself. +## Basic builtin types ```py -from typing_extensions import Never -from knot_extensions import is_disjoint_from, static_assert +from typing_extensions import Literal, LiteralString, Any +from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert -static_assert(is_disjoint_from(Never, Never)) -static_assert(is_disjoint_from(Never, None)) -static_assert(is_disjoint_from(Never, int)) -``` +static_assert(is_disjoint_from(bool, str)) +static_assert(not is_disjoint_from(bool, bool)) +static_assert(not is_disjoint_from(bool, int)) +static_assert(not is_disjoint_from(bool, object)) -## `None` +static_assert(not is_disjoint_from(Any, bool)) +static_assert(not is_disjoint_from(Any, Any)) -```py -from typing_extensions import Literal -from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert - -static_assert(is_disjoint_from(None, Literal[True])) -static_assert(is_disjoint_from(None, Literal[1])) -static_assert(is_disjoint_from(None, Literal["test"])) -static_assert(is_disjoint_from(None, Literal[b"test"])) -static_assert(is_disjoint_from(None, LiteralString)) -static_assert(is_disjoint_from(None, int)) -static_assert(is_disjoint_from(None, type[object])) -static_assert(is_disjoint_from(None, AlwaysTruthy)) - -static_assert(not is_disjoint_from(None, None)) -static_assert(not is_disjoint_from(None, object)) -static_assert(not is_disjoint_from(None, AlwaysFalsy)) -``` - -## Basic - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import AlwaysFalsy, AlwaysTruthy, Intersection, Not, TypeOf, is_disjoint_from, static_assert - -static_assert(is_disjoint_from(Literal[True], Literal[False])) -static_assert(is_disjoint_from(Literal[True], Literal[1])) -static_assert(is_disjoint_from(Literal[False], Literal[0])) - -static_assert(is_disjoint_from(Literal[1], Literal[2])) - -static_assert(is_disjoint_from(Literal["a"], Literal["b"])) - -static_assert(is_disjoint_from(Literal[b"a"], LiteralString)) -static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"])) -static_assert(is_disjoint_from(Literal[b"a"], Literal["a"])) - -static_assert(is_disjoint_from(type[object], TypeOf[Literal])) -static_assert(is_disjoint_from(type[str], LiteralString)) - -static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy)) - -static_assert(not is_disjoint_from(Any, int)) - -static_assert(not is_disjoint_from(Literal[True], Literal[True])) -static_assert(not is_disjoint_from(Literal[False], Literal[False])) -static_assert(not is_disjoint_from(Literal[True], bool)) -static_assert(not is_disjoint_from(Literal[True], int)) - -static_assert(not is_disjoint_from(Literal[1], Literal[1])) - -static_assert(not is_disjoint_from(Literal["a"], Literal["a"])) -static_assert(not is_disjoint_from(Literal["a"], LiteralString)) -static_assert(not is_disjoint_from(Literal["a"], str)) - -static_assert(not is_disjoint_from(int, int)) 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])) -static_assert(not is_disjoint_from(str, AlwaysFalsy)) -static_assert(not is_disjoint_from(str, AlwaysTruthy)) +``` + +## Class hierarchies + +```py +from knot_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of +from typing import final + +class A: ... +class B1(A): ... +class B2(A): ... + +# B1 and B2 are subclasses of A, so they are not disjoint from A: +static_assert(not is_disjoint_from(A, B1)) +static_assert(not is_disjoint_from(A, B2)) + +# The two subclasses B1 and B2 are also not disjoint ... +static_assert(not is_disjoint_from(B1, B2)) + +# ... because they could share a common subclass ... +class C(B1, B2): ... + +# ... which lies in their intersection: +static_assert(is_subtype_of(C, Intersection[B1, B2])) + +# However, if a class is marked final, it can not be subclassed ... +@final +class FinalSubclass(A): ... + +static_assert(not is_disjoint_from(FinalSubclass, A)) + +# ... which makes it disjoint from B1, B2: +static_assert(is_disjoint_from(B1, FinalSubclass)) +static_assert(is_disjoint_from(B2, FinalSubclass)) ``` ## Tuple types @@ -108,22 +83,124 @@ static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[ ```py from typing_extensions import Literal -from knot_extensions import AlwaysFalsy, AlwaysTruthy, Intersection, is_disjoint_from, static_assert +from knot_extensions import Intersection, is_disjoint_from, static_assert static_assert(is_disjoint_from(Literal[1, 2], Literal[3])) static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4])) -static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy)) - -static_assert(is_disjoint_from(Intersection[int, Literal[1]], Literal[2])) static_assert(not is_disjoint_from(Literal[1, 2], Literal[2])) static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) -static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy)) - -static_assert(not is_disjoint_from(Intersection[int, Literal[2]], Literal[2])) ``` -## Class, module and function literals +## Intersections + +```py +from typing_extensions import Literal, final +from knot_extensions import Intersection, is_disjoint_from, static_assert + +@final +class P: ... + +@final +class Q: ... + +@final +class R: ... + +# For three pairwise disjoint classes ... +static_assert(is_disjoint_from(P, Q)) +static_assert(is_disjoint_from(P, R)) +static_assert(is_disjoint_from(Q, R)) + +# ... their intersections are also disjoint: +static_assert(is_disjoint_from(Intersection[P, Q], R)) +static_assert(is_disjoint_from(Intersection[P, R], Q)) +static_assert(is_disjoint_from(Intersection[Q, R], P)) + +# On the other hand, for non-disjoint classes ... +class X: ... +class Y: ... +class Z: ... + +static_assert(not is_disjoint_from(X, Y)) +static_assert(not is_disjoint_from(X, Z)) +static_assert(not is_disjoint_from(Y, Z)) + +# ... their intersections are also not disjoint: +static_assert(not is_disjoint_from(Intersection[X, Y], Z)) +static_assert(not is_disjoint_from(Intersection[X, Z], Y)) +static_assert(not is_disjoint_from(Intersection[Y, Z], X)) +``` + +## Special types + +### `Never` + +`Never` is disjoint from every type, including itself. + +```py +from typing_extensions import Never +from knot_extensions import is_disjoint_from, static_assert + +static_assert(is_disjoint_from(Never, Never)) +static_assert(is_disjoint_from(Never, None)) +static_assert(is_disjoint_from(Never, int)) +static_assert(is_disjoint_from(Never, object)) +``` + +### `None` + +```py +from typing_extensions import Literal +from knot_extensions import is_disjoint_from, static_assert + +static_assert(is_disjoint_from(None, Literal[True])) +static_assert(is_disjoint_from(None, Literal[1])) +static_assert(is_disjoint_from(None, Literal["test"])) +static_assert(is_disjoint_from(None, Literal[b"test"])) +static_assert(is_disjoint_from(None, LiteralString)) +static_assert(is_disjoint_from(None, int)) +static_assert(is_disjoint_from(None, type[object])) + +static_assert(not is_disjoint_from(None, None)) +static_assert(not is_disjoint_from(None, int | None)) +static_assert(not is_disjoint_from(None, object)) +``` + +### Literals + +```py +from typing_extensions import Literal, LiteralString +from knot_extensions import TypeOf, is_disjoint_from, static_assert + +static_assert(is_disjoint_from(Literal[True], Literal[False])) +static_assert(is_disjoint_from(Literal[True], Literal[1])) +static_assert(is_disjoint_from(Literal[False], Literal[0])) + +static_assert(is_disjoint_from(Literal[1], Literal[2])) + +static_assert(is_disjoint_from(Literal["a"], Literal["b"])) + +static_assert(is_disjoint_from(Literal[b"a"], LiteralString)) +static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"])) +static_assert(is_disjoint_from(Literal[b"a"], Literal["a"])) + +static_assert(is_disjoint_from(type[object], TypeOf[Literal])) +static_assert(is_disjoint_from(type[str], LiteralString)) + +static_assert(not is_disjoint_from(Literal[True], Literal[True])) +static_assert(not is_disjoint_from(Literal[False], Literal[False])) +static_assert(not is_disjoint_from(Literal[True], bool)) +static_assert(not is_disjoint_from(Literal[True], int)) + +static_assert(not is_disjoint_from(Literal[1], Literal[1])) + +static_assert(not is_disjoint_from(Literal["a"], Literal["a"])) +static_assert(not is_disjoint_from(Literal["a"], LiteralString)) +static_assert(not is_disjoint_from(Literal["a"], str)) +``` + +### Class, module and function literals ```py from types import ModuleType, FunctionType @@ -132,9 +209,21 @@ from knot_extensions import TypeOf, is_disjoint_from, static_assert class A: ... class B: ... -static_assert(is_disjoint_from(TypeOf[A], TypeOf[B])) -static_assert(is_disjoint_from(TypeOf[A], type[B])) -static_assert(not is_disjoint_from(type[A], TypeOf[A])) +type LiteralA = TypeOf[A] +type LiteralB = TypeOf[B] + +# Class literals for different classes are always disjoint. +# They are singleton types that only contain the class object itself. +static_assert(is_disjoint_from(LiteralA, LiteralB)) + +# The class A is a subclass of A, so A is not disjoint from type[A]: +static_assert(not is_disjoint_from(LiteralA, type[A])) + +# The class A is disjoint from type[B] because it's not a subclass of B: +static_assert(is_disjoint_from(LiteralA, type[B])) + +# However, type[A] is not disjoint from type[B], as there could be +# classes that inherit from both A and B: static_assert(not is_disjoint_from(type[A], type[B])) import random @@ -152,7 +241,23 @@ static_assert(not is_disjoint_from(TypeOf[f], FunctionType)) static_assert(not is_disjoint_from(TypeOf[f], object)) ``` -## Instance types versus `type[T]` types +### `AlwaysTruthy` and `AlwaysFalsy` + +```py +from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert + +static_assert(is_disjoint_from(None, AlwaysTruthy)) +static_assert(not is_disjoint_from(None, AlwaysFalsy)) + +static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy)) +static_assert(not is_disjoint_from(str, AlwaysFalsy)) +static_assert(not is_disjoint_from(str, AlwaysTruthy)) + +static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy)) +static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy)) +``` + +### Instance types versus `type[T]` types An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of the instance type is not a subclass of `T`'s metaclass. @@ -185,7 +290,7 @@ static_assert(not is_disjoint_from(Meta2, type[UsesMeta2])) static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) ``` -## `type[T]` versus `type[S]` +### `type[T]` versus `type[S]` By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the metaclass of `S`.