Skip to content

stubtest: error if a class should be decorated with @final #12091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 30, 2022
18 changes: 18 additions & 0 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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(
Expand Down