Skip to content

Commit 3c4f624

Browse files
committed
Fix type checking decorated method overrides
The fix turned out to be pretty complicated, as there were a bunch of untested method overriding scenarios which weren't quite right. I fixed any related issues that I encountered, though I'm not certain whether some of the issues were hidden previously by other bugs. I also added tests for some related, previously untested scenarios. Fixes #1441.
1 parent abc228e commit 3c4f624

File tree

6 files changed

+271
-19
lines changed

6 files changed

+271
-19
lines changed

mypy/checker.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,13 +1015,13 @@ def expand_typevars(self, defn: FuncItem,
10151015
else:
10161016
return [(defn, typ)]
10171017

1018-
def check_method_override(self, defn: FuncBase) -> None:
1018+
def check_method_override(self, defn: Union[FuncBase, Decorator]) -> None:
10191019
"""Check if function definition is compatible with base classes."""
10201020
# Check against definitions in base classes.
10211021
for base in defn.info.mro[1:]:
10221022
self.check_method_or_accessor_override_for_base(defn, base)
10231023

1024-
def check_method_or_accessor_override_for_base(self, defn: FuncBase,
1024+
def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decorator],
10251025
base: TypeInfo) -> None:
10261026
"""Check if method definition is compatible with a base class."""
10271027
if base:
@@ -1041,13 +1041,26 @@ def check_method_or_accessor_override_for_base(self, defn: FuncBase,
10411041
base)
10421042

10431043
def check_method_override_for_base_with_name(
1044-
self, defn: FuncBase, name: str, base: TypeInfo) -> None:
1044+
self, defn: Union[FuncBase, Decorator], name: str, base: TypeInfo) -> None:
10451045
base_attr = base.names.get(name)
10461046
if base_attr:
10471047
# The name of the method is defined in the base class.
10481048

1049+
# Point errors at the 'def' line (important for backward compatibility
1050+
# of type ignores).
1051+
if not isinstance(defn, Decorator):
1052+
context = defn
1053+
else:
1054+
context = defn.func
10491055
# Construct the type of the overriding method.
1050-
typ = bind_self(self.function_type(defn), self.scope.active_self_type())
1056+
if isinstance(defn, FuncBase):
1057+
typ = self.function_type(defn) # type: Type
1058+
else:
1059+
assert defn.var.is_ready
1060+
assert defn.var.type is not None
1061+
typ = defn.var.type
1062+
if isinstance(typ, FunctionLike) and not is_static(context):
1063+
typ = bind_self(typ, self.scope.active_self_type())
10511064
# Map the overridden method type to subtype context so that
10521065
# it can be checked for compatibility.
10531066
original_type = base_attr.type
@@ -1058,23 +1071,31 @@ def check_method_override_for_base_with_name(
10581071
original_type = self.function_type(base_attr.node.func)
10591072
else:
10601073
assert False, str(base_attr.node)
1061-
if isinstance(original_type, FunctionLike):
1062-
original = map_type_from_supertype(
1063-
bind_self(original_type, self.scope.active_self_type()),
1064-
defn.info, base)
1074+
if isinstance(original_type, AnyType) or isinstance(typ, AnyType):
1075+
pass
1076+
elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike):
1077+
if (isinstance(base_attr.node, (FuncBase, Decorator))
1078+
and not is_static(base_attr.node)):
1079+
bound = bind_self(original_type, self.scope.active_self_type())
1080+
else:
1081+
bound = original_type
1082+
original = map_type_from_supertype(bound, defn.info, base)
10651083
# Check that the types are compatible.
10661084
# TODO overloaded signatures
10671085
self.check_override(typ,
10681086
cast(FunctionLike, original),
10691087
defn.name(),
10701088
name,
10711089
base.name(),
1072-
defn)
1073-
elif isinstance(original_type, AnyType):
1090+
context)
1091+
elif is_equivalent(original_type, typ):
1092+
# Assume invariance for a non-callable attribute here.
1093+
#
1094+
# TODO: Allow covariance for read-only attributes?
10741095
pass
10751096
else:
10761097
self.msg.signature_incompatible_with_supertype(
1077-
defn.name(), name, base.name(), defn)
1098+
defn.name(), name, base.name(), context)
10781099

10791100
def check_override(self, override: FunctionLike, original: FunctionLike,
10801101
name: str, name_in_super: str, supertype: str,
@@ -2364,9 +2385,11 @@ def visit_decorator(self, e: Decorator) -> None:
23642385
e.var.is_ready = True
23652386
return
23662387

2367-
e.func.accept(self)
2388+
self.check_func_item(e.func, name=e.func.name())
2389+
2390+
# Process decorators from the inside out to determine decorated signature, which
2391+
# may be different from the declared signature.
23682392
sig = self.function_type(e.func) # type: Type
2369-
# Process decorators from the inside out.
23702393
for d in reversed(e.decorators):
23712394
if refers_to_fullname(d, 'typing.overload'):
23722395
self.fail('Single overload definition, multiple required', e)
@@ -2387,6 +2410,8 @@ def visit_decorator(self, e: Decorator) -> None:
23872410
e.var.is_ready = True
23882411
if e.func.is_property:
23892412
self.check_incompatible_property_override(e)
2413+
if e.func.info and not e.func.is_dynamic():
2414+
self.check_method_override(e)
23902415

23912416
def check_for_untyped_decorator(self,
23922417
func: FuncDef,
@@ -3316,3 +3341,13 @@ def is_untyped_decorator(typ: Optional[Type]) -> bool:
33163341
if not typ or not isinstance(typ, CallableType):
33173342
return True
33183343
return typ.implicit
3344+
3345+
3346+
def is_static(func: Union[FuncBase, Decorator]) -> bool:
3347+
if isinstance(func, Decorator):
3348+
return is_static(func.func)
3349+
elif isinstance(func, OverloadedFuncDef):
3350+
return any(is_static(item) for item in func.items)
3351+
elif isinstance(func, FuncItem):
3352+
return func.is_static
3353+
return False

mypy/nodes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ class Decorator(SymbolNode, Statement):
565565
"""
566566

567567
func = None # type: FuncDef # Decorated function
568-
decorators = None # type: List[Expression] # Decorators, at least one # XXX Not true
568+
decorators = None # type: List[Expression] # Decorators (may be empty)
569569
var = None # type: Var # Represents the decorated function obj
570570
is_overload = False
571571

@@ -582,6 +582,10 @@ def name(self) -> str:
582582
def fullname(self) -> str:
583583
return self.func.fullname()
584584

585+
@property
586+
def info(self) -> 'TypeInfo':
587+
return self.func.info
588+
585589
def accept(self, visitor: StatementVisitor[T]) -> T:
586590
return visitor.visit_decorator(self)
587591

test-data/unit/check-abstract.test

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -728,13 +728,10 @@ class A(metaclass=ABCMeta):
728728
def x(self) -> int: pass
729729
class B(A):
730730
@property
731-
def x(self) -> str: pass # E
731+
def x(self) -> str: pass # E: Return type of "x" incompatible with supertype "A"
732732
b = B()
733-
b.x() # E
733+
b.x() # E: "str" not callable
734734
[builtins fixtures/property.pyi]
735-
[out]
736-
main:7: error: Return type of "x" incompatible with supertype "A"
737-
main:9: error: "str" not callable
738735

739736
[case testCantImplementAbstractPropertyViaInstanceVariable]
740737
from abc import abstractproperty, ABCMeta

test-data/unit/check-classes.test

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,136 @@ class A:
345345
class B(A):
346346
def __init_subclass__(cls) -> None: pass
347347

348+
[case testOverrideWithDecorator]
349+
from typing import Callable
350+
351+
def int_to_none(f: Callable[..., int]) -> Callable[..., None]: ...
352+
def str_to_int(f: Callable[..., str]) -> Callable[..., int]: ...
353+
354+
class A:
355+
def f(self) -> None: pass
356+
def g(self) -> str: pass
357+
def h(self) -> None: pass
358+
359+
class B(A):
360+
@int_to_none
361+
def f(self) -> int: pass
362+
@str_to_int
363+
def g(self) -> str: pass # E: Signature of "g" incompatible with supertype "A"
364+
@int_to_none
365+
@str_to_int
366+
def h(self) -> str: pass
367+
368+
[case testOverrideDecorated]
369+
from typing import Callable
370+
371+
def str_to_int(f: Callable[..., str]) -> Callable[..., int]: ...
372+
373+
class A:
374+
@str_to_int
375+
def f(self) -> str: pass
376+
@str_to_int
377+
def g(self) -> str: pass
378+
@str_to_int
379+
def h(self) -> str: pass
380+
381+
class B(A):
382+
def f(self) -> int: pass
383+
def g(self) -> str: pass # E: Signature of "g" incompatible with supertype "A"
384+
@str_to_int
385+
def h(self) -> str: pass
386+
387+
[case testOverrideWithDecoratorReturningAny]
388+
def dec(f): pass
389+
390+
class A:
391+
def f(self) -> str: pass
392+
393+
class B(A):
394+
@dec
395+
def f(self) -> int: pass
396+
397+
[case testOverrideWithDecoratorReturningInstance]
398+
def dec(f) -> str: pass
399+
400+
class A:
401+
def f(self) -> str: pass
402+
@dec
403+
def g(self) -> int: pass
404+
@dec
405+
def h(self) -> int: pass
406+
407+
class B(A):
408+
@dec
409+
def f(self) -> int: pass # E: Signature of "f" incompatible with supertype "A"
410+
def g(self) -> int: pass # E: Signature of "g" incompatible with supertype "A"
411+
@dec
412+
def h(self) -> str: pass
413+
414+
[case testOverrideStaticMethodWithStaticMethod]
415+
class A:
416+
@staticmethod
417+
def f(x: int, y: str) -> None: pass
418+
@staticmethod
419+
def g(x: int, y: str) -> None: pass
420+
421+
class B(A):
422+
@staticmethod
423+
def f(x: int, y: str) -> None: pass
424+
@staticmethod
425+
def g(x: str, y: str) -> None: pass # E: Argument 1 of "g" incompatible with supertype "A"
426+
[builtins fixtures/classmethod.pyi]
427+
428+
[case testOverrideClassMethodWithClassMethod]
429+
class A:
430+
@classmethod
431+
def f(cls, x: int, y: str) -> None: pass
432+
@classmethod
433+
def g(cls, x: int, y: str) -> None: pass
434+
435+
class B(A):
436+
@classmethod
437+
def f(cls, x: int, y: str) -> None: pass
438+
@classmethod
439+
def g(cls, x: str, y: str) -> None: pass # E: Argument 1 of "g" incompatible with supertype "A"
440+
[builtins fixtures/classmethod.pyi]
441+
442+
[case testOverrideClassMethodWithStaticMethod]
443+
class A:
444+
@classmethod
445+
def f(cls, x: int) -> None: pass
446+
@classmethod
447+
def g(cls, x: int) -> int: pass
448+
@classmethod
449+
def h(cls) -> int: pass
450+
451+
class B(A):
452+
@staticmethod
453+
def f(x: int) -> None: pass
454+
@staticmethod
455+
def g(x: str) -> int: pass # E: Argument 1 of "g" incompatible with supertype "A"
456+
@staticmethod
457+
def h() -> int: pass
458+
[builtins fixtures/classmethod.pyi]
459+
460+
[case testOverrideStaticMethodWithClassMethod]
461+
class A:
462+
@staticmethod
463+
def f(x: int) -> None: pass
464+
@staticmethod
465+
def g(x: str) -> int: pass
466+
@staticmethod
467+
def h() -> int: pass
468+
469+
class B(A):
470+
@classmethod
471+
def f(cls, x: int) -> None: pass
472+
@classmethod
473+
def g(cls, x: int) -> int: pass # E: Argument 1 of "g" incompatible with supertype "A"
474+
@classmethod
475+
def h(cls) -> int: pass
476+
[builtins fixtures/classmethod.pyi]
477+
348478

349479
-- Constructors
350480
-- ------------

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,42 @@ class A(B):
653653
x()
654654
[out]
655655

656+
[case testInvalidOverrideWithImplicitSignatureAndClassMethod1]
657+
class B:
658+
@classmethod
659+
def f(cls, x, y): pass
660+
class A(B):
661+
@classmethod
662+
def f(cls, x, y, z): pass # No error since no annotations
663+
[builtins fixtures/classmethod.pyi]
664+
665+
[case testInvalidOverrideWithImplicitSignatureAndClassMethod2]
666+
class B:
667+
@classmethod
668+
def f(cls, x: int, y): pass
669+
class A(B):
670+
@classmethod
671+
def f(cls, x, y, z): pass # No error since no annotations
672+
[builtins fixtures/classmethod.pyi]
673+
674+
[case testInvalidOverrideWithImplicitSignatureAndStaticMethod1]
675+
class B:
676+
@staticmethod
677+
def f(x, y): pass
678+
class A(B):
679+
@staticmethod
680+
def f(x, y, z): pass # No error since no annotations
681+
[builtins fixtures/classmethod.pyi]
682+
683+
[case testInvalidOverrideWithImplicitSignatureAndStaticMethod2]
684+
class B:
685+
@staticmethod
686+
def f(self, x: int, y): pass
687+
class A(B):
688+
@staticmethod
689+
def f(self, x, y, z): pass # No error since no annotations
690+
[builtins fixtures/classmethod.pyi]
691+
656692

657693
-- Don't complain about too few/many arguments in dynamic functions
658694
-- ----------------------------------------------------------------

test-data/unit/check-functions.test

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,56 @@ if x:
12761276
else:
12771277
def f(x: int = 0) -> None: pass # E: All conditional function variants must have identical signatures
12781278

1279+
[case testConditionalFunctionDefinitionUsingDecorator1]
1280+
from typing import Callable
1281+
1282+
def dec(f) -> Callable[[int], None]: pass
1283+
1284+
x = int()
1285+
if x:
1286+
@dec
1287+
def f(): pass
1288+
else:
1289+
def f(x: int) -> None: pass
1290+
1291+
[case testConditionalFunctionDefinitionUsingDecorator2]
1292+
from typing import Callable
1293+
1294+
def dec(f) -> Callable[[int], None]: pass
1295+
1296+
x = int()
1297+
if x:
1298+
@dec
1299+
def f(): pass
1300+
else:
1301+
def f(x: str) -> None: pass # E: Incompatible redefinition (redefinition with type Callable[[str], None], original type Callable[[int], None])
1302+
1303+
[case testConditionalFunctionDefinitionUsingDecorator3]
1304+
from typing import Callable
1305+
1306+
def dec(f) -> Callable[[int], None]: pass
1307+
1308+
x = int()
1309+
if x:
1310+
def f(x: int) -> None: pass
1311+
else:
1312+
# TODO: This should be okay.
1313+
@dec # E: Name 'f' already defined
1314+
def f(): pass
1315+
1316+
[case testConditionalFunctionDefinitionUsingDecorator4]
1317+
from typing import Callable
1318+
1319+
def dec(f) -> Callable[[int], None]: pass
1320+
1321+
x = int()
1322+
if x:
1323+
def f(x: str) -> None: pass
1324+
else:
1325+
# TODO: We should report an incompatible redefinition.
1326+
@dec # E: Name 'f' already defined
1327+
def f(): pass
1328+
12791329
[case testConditionalRedefinitionOfAnUnconditionalFunctionDefinition1]
12801330
from typing import Any
12811331
def f(x: str) -> None: pass

0 commit comments

Comments
 (0)