Skip to content

Commit 322fb10

Browse files
Add PEP 800 (disjoint bases) (#2262)
1 parent 268d0c4 commit 322fb10

File tree

8 files changed

+305
-0
lines changed

8 files changed

+305
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
conformant = "Pass"
2+
conformance_automated = "Pass"
3+
errors_diff = """
4+
"""
5+
output = """
6+
directives_disjoint_base.py:69: error: Class "LeftAndRight" has incompatible disjoint bases [misc]
7+
directives_disjoint_base.py:73: error: Class "LeftChildAndRight" has incompatible disjoint bases [misc]
8+
directives_disjoint_base.py:77: error: Class "LeftAndRightViaChild" has incompatible disjoint bases [misc]
9+
directives_disjoint_base.py:81: error: Class "LeftRecord" has incompatible disjoint bases [misc]
10+
directives_disjoint_base.py:105: error: Class "IncompatibleSlots" has incompatible disjoint bases [misc]
11+
directives_disjoint_base.py:113: error: Value of type variable "_TC" of "disjoint_base" cannot be "Callable[[], None]" [type-var]
12+
directives_disjoint_base.py:118: error: @disjoint_base cannot be used with TypedDict [misc]
13+
directives_disjoint_base.py:123: error: @disjoint_base cannot be used with protocol class [misc]
14+
"""
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
conformance_automated = "Fail"
2+
conformant = "Unsupported"
3+
notes = """
4+
Does not support PEP 800 disjoint-base semantics.
5+
"""
6+
errors_diff = """
7+
Line 69: Expected 1 errors
8+
Line 73: Expected 1 errors
9+
Line 77: Expected 1 errors
10+
Line 105: Expected 1 errors
11+
Line 118: Expected 1 errors
12+
Line 123: Expected 1 errors
13+
Line 60: Unexpected errors ['Named tuples do not support multiple inheritance [invalid-inheritance]']
14+
"""
15+
output = """
16+
ERROR directives_disjoint_base.py:60:7-18: Named tuples do not support multiple inheritance [invalid-inheritance]
17+
ERROR directives_disjoint_base.py:81:7-17: Named tuples do not support multiple inheritance [invalid-inheritance]
18+
ERROR directives_disjoint_base.py:113:1-15: `() -> None` is not assignable to upper bound `type[object]` of type variable `_TC` [bad-specialization]
19+
"""
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
conformance_automated = "Fail"
2+
conformant = "Unsupported"
3+
notes = """
4+
Does not support PEP 800 disjoint-base semantics.
5+
"""
6+
errors_diff = """
7+
Line 69: Expected 1 errors
8+
Line 73: Expected 1 errors
9+
Line 77: Expected 1 errors
10+
Line 81: Expected 1 errors
11+
Line 105: Expected 1 errors
12+
Line 118: Expected 1 errors
13+
Line 123: Expected 1 errors
14+
"""
15+
output = """
16+
directives_disjoint_base.py:113:2 - error: Argument of type "() -> None" cannot be assigned to parameter "cls" of type "_TC@disjoint_base" in function "disjoint_base"
17+
  Type "() -> None" is not assignable to type "type"
18+
    "FunctionType" is not assignable to "type" (reportArgumentType)
19+
directives_disjoint_base.py:135:22 - error: Argument of type "<subclass of Left and Right>" cannot be assigned to parameter "arg" of type "Never" in function "assert_never"
20+
  Type "<subclass of Left and Right>" is not assignable to type "Never" (reportArgumentType)
21+
"""

conformance/results/results.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,13 @@ <h3>Python Type System Conformance Test Results</h3>
11671167
<th class="column col2 conformant">Pass</th>
11681168
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not detect calls to deprecated overloads.</p><p>Does not detect implicit calls to deprecated dunder methods, for example via operators.</p><p>Does not detect accesses of, or attempts to set, deprecated properties.</p></span></div></th>
11691169
</tr>
1170+
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;directives_disjoint_base</th>
1171+
<th class="column col2 conformant">Pass</th>
1172+
<th class="column col2 not-conformant"><div class="hover-text">Unsupported<span class="tooltip-text" id="bottom"><p>Does not support PEP 800 disjoint-base semantics.</p></span></div></th>
1173+
<th class="column col2 not-conformant"><div class="hover-text">Unsupported<span class="tooltip-text" id="bottom"><p>Does not support PEP 800 disjoint-base semantics.</p></span></div></th>
1174+
<th class="column col2 not-conformant"><div class="hover-text">Unsupported<span class="tooltip-text" id="bottom"><p>Does not support PEP 800 disjoint-base semantics.</p></span></div></th>
1175+
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not reject @disjoint_base on TypedDict or Protocol definitions.</p></span></div></th>
1176+
</tr>
11701177
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;directives_no_type_check</th>
11711178
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not honor `@no_type_check` class decorator (allowed).</p><p>Does not reject invalid call of `@no_type_check` function.</p></span></div></th>
11721179
<th class="column col2 conformant"><div class="hover-text">Pass*<span class="tooltip-text" id="bottom"><p>Does not honor `@no_type_check` class decorator (allowed).</p></span></div></th>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
conformance_automated = "Fail"
2+
conformant = "Partial"
3+
notes = """
4+
Does not reject @disjoint_base on TypedDict or Protocol definitions.
5+
"""
6+
errors_diff = """
7+
Line 118: Expected 1 errors
8+
Line 123: Expected 1 errors
9+
"""
10+
output = """
11+
directives_disjoint_base.py:69:7: error[instance-layout-conflict] Class will raise `TypeError` at runtime due to incompatible bases: Bases `Left` and `Right` cannot be combined in multiple inheritance
12+
directives_disjoint_base.py:73:7: error[instance-layout-conflict] Class will raise `TypeError` at runtime due to incompatible bases: Bases `LeftChild` and `Right` cannot be combined in multiple inheritance
13+
directives_disjoint_base.py:77:7: error[instance-layout-conflict] Class will raise `TypeError` at runtime due to incompatible bases: Bases `LeftAndPlain` and `Right` cannot be combined in multiple inheritance
14+
directives_disjoint_base.py:81:7: error[instance-layout-conflict] Class will raise `TypeError` at runtime due to incompatible bases: Bases `Left` and `Record` cannot be combined in multiple inheritance
15+
directives_disjoint_base.py:105:7: error[instance-layout-conflict] Class will raise `TypeError` at runtime due to incompatible bases: Bases `SlotBase1` and `SlotBase2` cannot be combined in multiple inheritance
16+
directives_disjoint_base.py:113:1: error[invalid-argument-type] Argument to function `disjoint_base` is incorrect: Argument type `def func() -> None` does not satisfy upper bound `type` of type variable `_TC`
17+
"""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
conformance_automated = "Fail"
2+
conformant = "Unsupported"
3+
notes = """
4+
Does not support PEP 800 disjoint-base semantics.
5+
"""
6+
errors_diff = """
7+
Line 69: Expected 1 errors
8+
Line 73: Expected 1 errors
9+
Line 77: Expected 1 errors
10+
Line 81: Expected 1 errors
11+
Line 105: Expected 1 errors
12+
Line 118: Expected 1 errors
13+
Line 123: Expected 1 errors
14+
"""
15+
output = """
16+
directives_disjoint_base.py:113: error: Value of type variable "_TC" of "disjoint_base" cannot be "Callable[[], None]" [type-var]
17+
directives_disjoint_base.py:135: error: Argument 1 to "assert_never" has incompatible type "<subclass of "tests.directives_disjoint_base.Left" and "tests.directives_disjoint_base.Right">"; expected "Never" [arg-type]
18+
"""
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Tests the typing.disjoint_base decorator introduced in PEP 800.
3+
"""
4+
5+
# Specification: https://typing.readthedocs.io/en/latest/spec/directives.html#disjoint-base
6+
# See also https://peps.python.org/pep-0800/
7+
8+
from typing import NamedTuple, Protocol, TypedDict, assert_never
9+
from typing_extensions import disjoint_base
10+
11+
12+
# > It may only be used on nominal classes, including ``NamedTuple``
13+
# > definitions
14+
15+
16+
@disjoint_base
17+
class Left:
18+
pass
19+
20+
21+
@disjoint_base
22+
class Right:
23+
pass
24+
25+
26+
@disjoint_base
27+
class LeftChild(Left):
28+
pass
29+
30+
31+
@disjoint_base
32+
class Record(NamedTuple):
33+
value: int
34+
35+
36+
class Plain:
37+
pass
38+
39+
40+
# > If the candidate set contains a single disjoint base, that is the
41+
# > class's disjoint base.
42+
43+
44+
class OtherLeftChild(Left):
45+
pass
46+
47+
48+
# > If there are multiple candidates, but one of them is a subclass of
49+
# > all other candidates, that class is the disjoint base.
50+
51+
52+
class LeftAndPlain(Left, Plain):
53+
pass
54+
55+
56+
class LeftChildAndLeft(LeftChild, Left):
57+
pass
58+
59+
60+
class PlainRecord(Plain, Record):
61+
pass
62+
63+
64+
# > Type checkers must check for a valid disjoint base when checking class definitions,
65+
# > and emit a diagnostic if they encounter a class
66+
# > definition that lacks a valid disjoint base.
67+
68+
69+
class LeftAndRight(Left, Right): # E: incompatible disjoint bases
70+
pass
71+
72+
73+
class LeftChildAndRight(LeftChild, Right): # E: incompatible disjoint bases
74+
pass
75+
76+
77+
class LeftAndRightViaChild(LeftAndPlain, Right): # E: incompatible disjoint bases
78+
pass
79+
80+
81+
class LeftRecord(Left, Record): # E: incompatible disjoint bases
82+
pass
83+
84+
85+
# > A nominal class is a disjoint base if it [...] contains a non-empty
86+
# > `__slots__` definition.
87+
88+
89+
class SlotBase1:
90+
__slots__ = ("x",)
91+
92+
93+
class SlotBase2:
94+
__slots__ = ("y",)
95+
96+
97+
class EmptySlots:
98+
__slots__ = ()
99+
100+
101+
class SlotAndEmptySlots(SlotBase1, EmptySlots):
102+
pass
103+
104+
105+
class IncompatibleSlots(SlotBase1, SlotBase2): # E: incompatible disjoint bases
106+
pass
107+
108+
109+
# > it is a type checker error to use the decorator on a function,
110+
# > ``TypedDict`` definition, or ``Protocol`` definition.
111+
112+
113+
@disjoint_base # E: disjoint_base cannot be applied to a function
114+
def func() -> None:
115+
pass
116+
117+
118+
@disjoint_base # E: disjoint_base cannot be applied to a TypedDict
119+
class Movie(TypedDict):
120+
name: str
121+
122+
123+
@disjoint_base # E: disjoint_base cannot be applied to a Protocol
124+
class SupportsClose(Protocol):
125+
def close(self) -> None:
126+
...
127+
128+
129+
# > Type checkers may use disjoint bases to determine that two classes cannot
130+
# > have a common subclass.
131+
132+
133+
def narrow(obj: Left) -> None:
134+
if isinstance(obj, Right): # E?: may be treated as unreachable
135+
assert_never(obj) # E?: may not be narrowed to Never

docs/spec/directives.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,77 @@ deprecated functionality in their CI pipeline. Therefore, it is recommended
294294
that type checkers provide configuration options that cover both use cases.
295295
As with any other type checker error, it is also possible to ignore deprecations
296296
using ``# type: ignore`` comments.
297+
298+
.. _`disjoint-base`:
299+
300+
``@disjoint_base``
301+
------------------
302+
303+
(Originally specified in :pep:`800`.)
304+
305+
The ``@typing.disjoint_base`` decorator may be used to mark a nominal class as
306+
a disjoint base. It may only be used on nominal classes, including ``NamedTuple``
307+
definitions; it is a type checker error to use the decorator on a function,
308+
``TypedDict`` definition, or ``Protocol`` definition.
309+
310+
We define two properties on (nominal) classes: a class may or may not *be* a
311+
disjoint base, and every class must *have* a valid disjoint base.
312+
313+
A nominal class is a disjoint base if it is decorated with ``@typing.disjoint_base``,
314+
or if it contains a non-empty ``__slots__`` definition.
315+
This includes classes that have ``__slots__`` because of the ``@dataclass(slots=True)`` decorator or
316+
because of the use of the ``dataclass_transform`` mechanism to add slots.
317+
The universal base class, ``object``, is also a disjoint base.
318+
319+
To determine a class's disjoint base, we look at all of its base classes to
320+
determine a set of candidate disjoint bases. For each base
321+
that is itself a disjoint base, the candidate is the base itself; otherwise,
322+
it is the base's disjoint base. If the candidate set contains
323+
a single disjoint base, that is the class's disjoint base. If there are multiple
324+
candidates, but one of them is a subclass of all other candidates,
325+
that class is the disjoint base. If no such candidate exists, the class does not
326+
have a valid disjoint base, and therefore cannot exist.
327+
328+
Type checkers must check for a valid disjoint base when checking class definitions,
329+
and emit a diagnostic if they encounter a class
330+
definition that lacks a valid disjoint base. Type checkers may also use the disjoint
331+
base mechanism to determine whether types are disjoint,
332+
for example when checking whether a type narrowing construct like ``isinstance()``
333+
results in an unreachable branch.
334+
335+
Example::
336+
337+
from typing import disjoint_base, assert_never
338+
339+
@disjoint_base
340+
class Disjoint1:
341+
pass
342+
343+
@disjoint_base
344+
class Disjoint2:
345+
pass
346+
347+
@disjoint_base
348+
class DisjointChild(Disjoint1):
349+
pass
350+
351+
class C1: # disjoint base is `object`
352+
pass
353+
354+
# OK: candidate disjoint bases are `Disjoint1` and `object`, and `Disjoint1` is a subclass of `object`.
355+
class C2(Disjoint1, C1): # disjoint base is `Disjoint1`
356+
pass
357+
358+
# OK: candidate disjoint bases are `DisjointChild` and `Disjoint1`, and `DisjointChild` is a subclass of `Disjoint1`.
359+
class C3(DisjointChild, Disjoint1): # disjoint base is `DisjointChild`
360+
pass
361+
362+
# error: candidate disjoint bases are `Disjoint1` and `Disjoint2`, but neither is a subclass of the other
363+
class C4(Disjoint1, Disjoint2):
364+
pass
365+
366+
def narrower(obj: Disjoint1) -> None:
367+
if isinstance(obj, Disjoint2):
368+
assert_never(obj) # OK: child class of `Disjoint1` and `Disjoint2` cannot exist
369+
if isinstance(obj, C1):
370+
reveal_type(obj) # Shows a non-empty type, e.g. `Disjoint1 & C1`

0 commit comments

Comments
 (0)