Skip to content

Commit 25087fd

Browse files
authored
Validate more about overrides on untyped methods (#17276)
This commit fixes #9618 by making MyPy always complain if a method overrides a base class method marked as `@final`. In the process, it also adds a few additional validations: - Always verify the `@override` decorator, which ought to be pretty backward-compatible for most projects assuming that strict override checks aren't enabled by default (and it appears to me that `--enable-error-code explicit-override` is off by default) - Verify that the method signature is compatible (which in practice means only arity and argument name checks) *if* the `--check-untyped-defs` flag is set; it seems unlikely that a user would want mypy to validate the bodies of untyped functions but wouldn't want to be alerted about incompatible overrides. Note: I did also explore enabling the signature compatibility check for all code, which in principle makes sense. But the mypy_primer results indicated that there would be backward compability issues because too many libraries rely on us not validating this: #17274
1 parent 0871c93 commit 25087fd

File tree

4 files changed

+49
-7
lines changed

4 files changed

+49
-7
lines changed

mypy/checker.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ def _visit_func_def(self, defn: FuncDef) -> None:
10041004
"""Type check a function definition."""
10051005
self.check_func_item(defn, name=defn.name)
10061006
if defn.info:
1007-
if not defn.is_dynamic() and not defn.is_overload and not defn.is_decorated:
1007+
if not defn.is_overload and not defn.is_decorated:
10081008
# If the definition is the implementation for an
10091009
# overload, the legality of the override has already
10101010
# been typechecked, and decorated methods will be
@@ -1913,9 +1913,17 @@ def check_method_override(
19131913
Return a list of base classes which contain an attribute with the method name.
19141914
"""
19151915
# Check against definitions in base classes.
1916+
check_override_compatibility = defn.name not in (
1917+
"__init__",
1918+
"__new__",
1919+
"__init_subclass__",
1920+
"__post_init__",
1921+
) and (self.options.check_untyped_defs or not defn.is_dynamic())
19161922
found_method_base_classes: list[TypeInfo] = []
19171923
for base in defn.info.mro[1:]:
1918-
result = self.check_method_or_accessor_override_for_base(defn, base)
1924+
result = self.check_method_or_accessor_override_for_base(
1925+
defn, base, check_override_compatibility
1926+
)
19191927
if result is None:
19201928
# Node was deferred, we will have another attempt later.
19211929
return None
@@ -1924,7 +1932,10 @@ def check_method_override(
19241932
return found_method_base_classes
19251933

19261934
def check_method_or_accessor_override_for_base(
1927-
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
1935+
self,
1936+
defn: FuncDef | OverloadedFuncDef | Decorator,
1937+
base: TypeInfo,
1938+
check_override_compatibility: bool,
19281939
) -> bool | None:
19291940
"""Check if method definition is compatible with a base class.
19301941
@@ -1945,10 +1956,8 @@ def check_method_or_accessor_override_for_base(
19451956
if defn.is_final:
19461957
self.check_if_final_var_override_writable(name, base_attr.node, defn)
19471958
found_base_method = True
1948-
1949-
# Check the type of override.
1950-
if name not in ("__init__", "__new__", "__init_subclass__", "__post_init__"):
1951-
# Check method override
1959+
if check_override_compatibility:
1960+
# Check compatibility of the override signature
19521961
# (__init__, __new__, __init_subclass__ are special).
19531962
if self.check_method_override_for_base_with_name(defn, name, base):
19541963
return None

mypy/nodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef:
616616
# NOTE: res.info will be set in the fixup phase.
617617
return res
618618

619+
def is_dynamic(self) -> bool:
620+
return all(item.is_dynamic() for item in self.items)
621+
619622

620623
class Argument(Node):
621624
"""A single argument in a FuncItem."""
@@ -938,6 +941,9 @@ def deserialize(cls, data: JsonDict) -> Decorator:
938941
dec.is_overload = data["is_overload"]
939942
return dec
940943

944+
def is_dynamic(self) -> bool:
945+
return self.func.is_dynamic()
946+
941947

942948
VAR_FLAGS: Final = [
943949
"is_self",

test-data/unit/check-dynamic-typing.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,21 @@ main:5: note: def f(self, x: A) -> None
756756
main:5: note: Subclass:
757757
main:5: note: def f(self, x: Any, y: Any) -> None
758758

759+
[case testInvalidOverrideArgumentCountWithImplicitSignature4]
760+
# flags: --check-untyped-defs
761+
import typing
762+
class B:
763+
def f(self, x: A) -> None: pass
764+
class A(B):
765+
def f(self, x, y):
766+
x()
767+
[out]
768+
main:6: error: Signature of "f" incompatible with supertype "B"
769+
main:6: note: Superclass:
770+
main:6: note: def f(self, x: A) -> None
771+
main:6: note: Subclass:
772+
main:6: note: def f(self, x: Any, y: Any) -> Any
773+
759774
[case testInvalidOverrideWithImplicitSignatureAndClassMethod1]
760775
class B:
761776
@classmethod

test-data/unit/check-functions.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3228,3 +3228,15 @@ class A:
32283228
reveal_type(A.f) # N: Revealed type is "__main__.something_callable"
32293229
reveal_type(A().f) # N: Revealed type is "builtins.str"
32303230
[builtins fixtures/property.pyi]
3231+
3232+
[case testFinalOverrideOnUntypedDef]
3233+
from typing import final
3234+
3235+
class Base:
3236+
@final
3237+
def foo(self):
3238+
pass
3239+
3240+
class Derived(Base):
3241+
def foo(self): # E: Cannot override final attribute "foo" (previously declared in base class "Base")
3242+
pass

0 commit comments

Comments
 (0)