Skip to content

Commit 080bb0e

Browse files
authored
stubtest: error if a class should be decorated with @Final (#12091)
1 parent 8f9ebf0 commit 080bb0e

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

mypy/stubtest.py

+18
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,24 @@ def verify_typeinfo(
254254
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
255255
return
256256

257+
try:
258+
class SubClass(runtime): # type: ignore
259+
pass
260+
except TypeError:
261+
# Enum classes are implicitly @final
262+
if not stub.is_final and not issubclass(runtime, enum.Enum):
263+
yield Error(
264+
object_path,
265+
"cannot be subclassed at runtime, but isn't marked with @final in the stub",
266+
stub,
267+
runtime,
268+
stub_desc=repr(stub),
269+
)
270+
except Exception:
271+
# The class probably wants its subclasses to do something special.
272+
# Examples: ctypes.Array, ctypes._SimpleCData
273+
pass
274+
257275
# Check everything already defined in the stub
258276
to_check = set(stub.names)
259277
# There's a reasonable case to be made that we should always check all dunders, but it's

mypy/test/teststubtest.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,13 @@ def test_special_dunders(self) -> Iterator[Case]:
691691
)
692692
if sys.version_info >= (3, 6):
693693
yield Case(
694-
stub="class C:\n def __init_subclass__(cls, e: int, **kwargs: int) -> None: ...",
695-
runtime="class C:\n def __init_subclass__(cls, e, **kwargs): pass",
694+
stub=(
695+
"class C:\n"
696+
" def __init_subclass__(\n"
697+
" cls, e: int = ..., **kwargs: int\n"
698+
" ) -> None: ...\n"
699+
),
700+
runtime="class C:\n def __init_subclass__(cls, e=1, **kwargs): pass",
696701
error=None,
697702
)
698703
if sys.version_info >= (3, 9):
@@ -702,6 +707,28 @@ def test_special_dunders(self) -> Iterator[Case]:
702707
error=None,
703708
)
704709

710+
def test_not_subclassable(self) -> None:
711+
output = run_stubtest(
712+
stub=(
713+
"class CanBeSubclassed: ...\n"
714+
"class CanNotBeSubclassed:\n"
715+
" def __init_subclass__(cls) -> None: ...\n"
716+
),
717+
runtime=(
718+
"class CanNotBeSubclassed:\n"
719+
" def __init_subclass__(cls): raise TypeError('nope')\n"
720+
# ctypes.Array can be subclassed, but subclasses must define a few
721+
# special attributes, e.g. _length_
722+
"from ctypes import Array as CanBeSubclassed\n"
723+
),
724+
options=[],
725+
)
726+
assert (
727+
"CanNotBeSubclassed cannot be subclassed at runtime,"
728+
" but isn't marked with @final in the stub"
729+
) in output
730+
assert "CanBeSubclassed cannot be subclassed" not in output
731+
705732
@collect_cases
706733
def test_name_mangling(self) -> Iterator[Case]:
707734
yield Case(

0 commit comments

Comments
 (0)