Skip to content

Commit fa7fa7f

Browse files
stubtest: flag redundant @disjoint_base decorators (#19715)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 35a15b1 commit fa7fa7f

File tree

2 files changed

+91
-20
lines changed

2 files changed

+91
-20
lines changed

mypy/stubtest.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -506,28 +506,57 @@ def _is_disjoint_base(typ: type[object]) -> bool:
506506
def _verify_disjoint_base(
507507
stub: nodes.TypeInfo, runtime: type[object], object_path: list[str]
508508
) -> Iterator[Error]:
509-
# If it's final, doesn't matter whether it's a disjoint base or not
510-
if stub.is_final:
511-
return
512509
is_disjoint_runtime = _is_disjoint_base(runtime)
513510
# Don't complain about missing @disjoint_base if there are __slots__, because
514511
# in that case we can infer that it's a disjoint base.
515-
if is_disjoint_runtime and not stub.is_disjoint_base and not runtime.__dict__.get("__slots__"):
512+
if (
513+
is_disjoint_runtime
514+
and not stub.is_disjoint_base
515+
and not runtime.__dict__.get("__slots__")
516+
and not stub.is_final
517+
and not (stub.is_enum and stub.enum_members)
518+
):
516519
yield Error(
517520
object_path,
518521
"is a disjoint base at runtime, but isn't marked with @disjoint_base in the stub",
519522
stub,
520523
runtime,
521524
stub_desc=repr(stub),
522525
)
523-
elif not is_disjoint_runtime and stub.is_disjoint_base:
524-
yield Error(
525-
object_path,
526-
"is marked with @disjoint_base in the stub, but isn't a disjoint base at runtime",
527-
stub,
528-
runtime,
529-
stub_desc=repr(stub),
530-
)
526+
elif stub.is_disjoint_base:
527+
if not is_disjoint_runtime:
528+
yield Error(
529+
object_path,
530+
"is marked with @disjoint_base in the stub, but isn't a disjoint base at runtime",
531+
stub,
532+
runtime,
533+
stub_desc=repr(stub),
534+
)
535+
if runtime.__dict__.get("__slots__"):
536+
yield Error(
537+
object_path,
538+
"is marked as @disjoint_base, but also has slots; add __slots__ instead",
539+
stub,
540+
runtime,
541+
stub_desc=repr(stub),
542+
)
543+
elif stub.is_final:
544+
yield Error(
545+
object_path,
546+
"is marked as @disjoint_base, but also marked as @final; remove @disjoint_base",
547+
stub,
548+
runtime,
549+
stub_desc=repr(stub),
550+
)
551+
elif stub.is_enum and stub.enum_members:
552+
yield Error(
553+
object_path,
554+
"is marked as @disjoint_base, but is an enum with members, which is implicitly final; "
555+
"remove @disjoint_base",
556+
stub,
557+
runtime,
558+
stub_desc=repr(stub),
559+
)
531560

532561

533562
def _verify_metaclass(

mypy/test/teststubtest.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,14 +1407,9 @@ def spam(x=Flags4(0)): pass
14071407
stub="""
14081408
import sys
14091409
from typing import Final, Literal
1410-
from typing_extensions import disjoint_base
1411-
if sys.version_info >= (3, 12):
1412-
class BytesEnum(bytes, enum.Enum):
1413-
a = b'foo'
1414-
else:
1415-
@disjoint_base
1416-
class BytesEnum(bytes, enum.Enum):
1417-
a = b'foo'
1410+
class BytesEnum(bytes, enum.Enum):
1411+
a = b'foo'
1412+
14181413
FOO: Literal[BytesEnum.a]
14191414
BAR: Final = BytesEnum.a
14201415
BAZ: BytesEnum
@@ -1698,6 +1693,53 @@ def __next__(self) -> object: ...
16981693
""",
16991694
error=None,
17001695
)
1696+
yield Case(
1697+
runtime="""
1698+
class IsDisjointBaseBecauseItHasSlots:
1699+
__slots__ = ("a",)
1700+
a: int
1701+
""",
1702+
stub="""
1703+
from typing_extensions import disjoint_base
1704+
1705+
@disjoint_base
1706+
class IsDisjointBaseBecauseItHasSlots:
1707+
a: int
1708+
""",
1709+
error="test_module.IsDisjointBaseBecauseItHasSlots",
1710+
)
1711+
yield Case(
1712+
runtime="""
1713+
class IsFinalSoDisjointBaseIsRedundant: ...
1714+
""",
1715+
stub="""
1716+
from typing_extensions import disjoint_base, final
1717+
1718+
@final
1719+
@disjoint_base
1720+
class IsFinalSoDisjointBaseIsRedundant: ...
1721+
""",
1722+
error="test_module.IsFinalSoDisjointBaseIsRedundant",
1723+
)
1724+
yield Case(
1725+
runtime="""
1726+
import enum
1727+
1728+
class IsEnumWithMembersSoDisjointBaseIsRedundant(enum.Enum):
1729+
A = 1
1730+
B = 2
1731+
""",
1732+
stub="""
1733+
from typing_extensions import disjoint_base
1734+
import enum
1735+
1736+
@disjoint_base
1737+
class IsEnumWithMembersSoDisjointBaseIsRedundant(enum.Enum):
1738+
A = 1
1739+
B = 2
1740+
""",
1741+
error="test_module.IsEnumWithMembersSoDisjointBaseIsRedundant",
1742+
)
17011743

17021744
@collect_cases
17031745
def test_has_runtime_final_decorator(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)