diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4ca088d8aa3e..df36935a801f 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -254,6 +254,24 @@ def verify_typeinfo( yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub)) return + try: + class SubClass(runtime): # type: ignore + pass + except TypeError: + # Enum classes are implicitly @final + if not stub.is_final and not issubclass(runtime, enum.Enum): + yield Error( + object_path, + "cannot be subclassed at runtime, but isn't marked with @final in the stub", + stub, + runtime, + stub_desc=repr(stub), + ) + except Exception: + # The class probably wants its subclasses to do something special. + # Examples: ctypes.Array, ctypes._SimpleCData + pass + # Check everything already defined in the stub to_check = set(stub.names) # There's a reasonable case to be made that we should always check all dunders, but it's diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 30ed953d7390..2852299548ed 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -691,8 +691,13 @@ def test_special_dunders(self) -> Iterator[Case]: ) if sys.version_info >= (3, 6): yield Case( - stub="class C:\n def __init_subclass__(cls, e: int, **kwargs: int) -> None: ...", - runtime="class C:\n def __init_subclass__(cls, e, **kwargs): pass", + stub=( + "class C:\n" + " def __init_subclass__(\n" + " cls, e: int = ..., **kwargs: int\n" + " ) -> None: ...\n" + ), + runtime="class C:\n def __init_subclass__(cls, e=1, **kwargs): pass", error=None, ) if sys.version_info >= (3, 9): @@ -702,6 +707,28 @@ def test_special_dunders(self) -> Iterator[Case]: error=None, ) + def test_not_subclassable(self) -> None: + output = run_stubtest( + stub=( + "class CanBeSubclassed: ...\n" + "class CanNotBeSubclassed:\n" + " def __init_subclass__(cls) -> None: ...\n" + ), + runtime=( + "class CanNotBeSubclassed:\n" + " def __init_subclass__(cls): raise TypeError('nope')\n" + # ctypes.Array can be subclassed, but subclasses must define a few + # special attributes, e.g. _length_ + "from ctypes import Array as CanBeSubclassed\n" + ), + options=[], + ) + assert ( + "CanNotBeSubclassed cannot be subclassed at runtime," + " but isn't marked with @final in the stub" + ) in output + assert "CanBeSubclassed cannot be subclassed" not in output + @collect_cases def test_name_mangling(self) -> Iterator[Case]: yield Case(