diff --git a/mypy/checker.py b/mypy/checker.py index b90221a0a5a53..130e420eba881 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -42,7 +42,8 @@ from mypy.sametypes import is_same_type from mypy.messages import ( MessageBuilder, make_inferred_type_note, append_invariance_notes, pretty_seq, - format_type, format_type_bare, format_type_distinctly, SUGGESTED_TEST_FIXTURES + format_type, format_type_bare, format_type_distinctly, SUGGESTED_TEST_FIXTURES, + SubtypingContext, ClassOrStaticContext, ) import mypy.checkexpr from mypy.checkmember import ( @@ -1528,13 +1529,13 @@ def check_method_override_for_base_with_name( # Construct the type of the overriding method. if isinstance(defn, (FuncDef, OverloadedFuncDef)): typ: Type = self.function_type(defn) - override_class_or_static = defn.is_class or defn.is_static + override_static = defn.is_static override_class = defn.is_class else: assert defn.var.is_ready assert defn.var.type is not None typ = defn.var.type - override_class_or_static = defn.func.is_class or defn.func.is_static + override_static = defn.func.is_static override_class = defn.func.is_class typ = get_proper_type(typ) if isinstance(typ, FunctionLike) and not is_static(context): @@ -1567,27 +1568,35 @@ def check_method_override_for_base_with_name( else: assert False, str(base_attr.node) if isinstance(original_node, (FuncDef, OverloadedFuncDef)): - original_class_or_static = original_node.is_class or original_node.is_static + original_class = original_node.is_class + original_static = original_node.is_static elif isinstance(original_node, Decorator): fdef = original_node.func - original_class_or_static = fdef.is_class or fdef.is_static + original_class = fdef.is_class + original_static = fdef.is_static else: - original_class_or_static = False # a variable can't be class or static + original_class = original_static = False # a variable can't be class or static + + subtyping_context = SubtypingContext( + original=ClassOrStaticContext(original_class, original_static), + override=ClassOrStaticContext(override_class, override_static), + ) + if isinstance(original_type, AnyType) or isinstance(typ, AnyType): pass elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike): original = self.bind_and_map_method(base_attr, original_type, defn.info, base) # Check that the types are compatible. - # TODO overloaded signatures - self.check_override(typ, - original, - defn.name, - name, - base.name, - original_class_or_static, - override_class_or_static, - context) + self.check_override( + typ, + original, + defn.name, + name, + base.name, + context, + subtyping_context=subtyping_context, + ) elif is_equivalent(original_type, typ): # Assume invariance for a non-callable attribute here. Note # that this doesn't affect read-only properties which can have @@ -1600,7 +1609,9 @@ def check_method_override_for_base_with_name( pass else: self.msg.signature_incompatible_with_supertype( - defn.name, name, base.name, context) + defn.name, name, base.name, context, + subtyping_context=subtyping_context, + ) return False def bind_and_map_method(self, sym: SymbolTableNode, typ: FunctionLike, @@ -1640,9 +1651,9 @@ def get_op_other_domain(self, tp: FunctionLike) -> Optional[Type]: def check_override(self, override: FunctionLike, original: FunctionLike, name: str, name_in_super: str, supertype: str, - original_class_or_static: bool, - override_class_or_static: bool, - node: Context) -> None: + node: Context, + *, + subtyping_context: SubtypingContext) -> None: """Check a method override with given signatures. Arguments: @@ -1667,6 +1678,14 @@ def check_override(self, override: FunctionLike, original: FunctionLike, fail = True op_method_wider_note = True if isinstance(original, FunctionLike) and isinstance(override, FunctionLike): + original_class_or_static = ( + subtyping_context.original.is_class + or subtyping_context.original.is_static + ) + override_class_or_static = ( + subtyping_context.override.is_class + or subtyping_context.override.is_static + ) if original_class_or_static and not override_class_or_static: fail = True elif isinstance(original, CallableType) and isinstance(override, CallableType): @@ -1743,7 +1762,10 @@ def erase_override(t: Type) -> Type: if not emitted_msg: # Fall back to generic incompatibility message. self.msg.signature_incompatible_with_supertype( - name, name_in_super, supertype, node, original=original, override=override) + name, name_in_super, supertype, node, + original=original, override=override, + subtyping_context=subtyping_context, + ) if op_method_wider_note: self.note("Overloaded operator methods can't have wider argument types" " in overrides", node, code=codes.OVERRIDE) diff --git a/mypy/messages.py b/mypy/messages.py index da284cc88ba43..85b8bc618056a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -15,7 +15,10 @@ import difflib from textwrap import dedent -from typing import cast, List, Dict, Any, Sequence, Iterable, Iterator, Tuple, Set, Optional, Union +from typing import ( + cast, List, Dict, Any, Sequence, Iterable, Iterator, + Tuple, Set, Optional, Union, NamedTuple, +) from typing_extensions import Final from mypy.erasetype import erase_type @@ -86,6 +89,24 @@ } +class ClassOrStaticContext(NamedTuple): + """We use this type for better error messages with `@classmethod` and `@staticmethod`. + + The problem is: we cannot just rely on the regular metadata, + since it is not serialized / deserialized properly. See #11791 + """ + + is_class: bool + is_static: bool + + +class SubtypingContext(NamedTuple): + """Container we use when working with incorrect subtyping message.""" + + original: ClassOrStaticContext + override: ClassOrStaticContext + + class MessageBuilder: """Helper class for reporting type checker error messages with parameters. @@ -817,6 +838,7 @@ def overload_signature_incompatible_with_supertype( def signature_incompatible_with_supertype( self, name: str, name_in_super: str, supertype: str, context: Context, + subtyping_context: SubtypingContext, original: Optional[FunctionLike] = None, override: Optional[FunctionLike] = None) -> None: code = codes.OVERRIDE @@ -824,7 +846,6 @@ def signature_incompatible_with_supertype( self.fail('Signature of "{}" incompatible with {}'.format( name, target), context, code=code) - INCLUDE_DECORATOR = True # Include @classmethod and @staticmethod decorators, if any ALLOW_DUPS = True # Allow duplicate notes, needed when signatures are duplicates ALIGN_OFFSET = 1 # One space, to account for the difference between error and note OFFSET = 4 # Four spaces, so that notes will look like this: @@ -833,36 +854,36 @@ def signature_incompatible_with_supertype( # note: def f(self) -> str # note: Subclass: # note: def f(self, x: str) -> None - if original is not None and isinstance(original, (CallableType, Overloaded)) \ - and override is not None and isinstance(override, (CallableType, Overloaded)): + if isinstance(original, FunctionLike) and isinstance(override, FunctionLike): self.note('Superclass:', context, offset=ALIGN_OFFSET + OFFSET, code=code) - self.pretty_callable_or_overload(original, context, offset=ALIGN_OFFSET + 2 * OFFSET, - add_class_or_static_decorator=INCLUDE_DECORATOR, - allow_dups=ALLOW_DUPS, code=code) + self.pretty_callable_or_overload( + original, context, offset=ALIGN_OFFSET + 2 * OFFSET, + class_or_static=subtyping_context.original, + allow_dups=ALLOW_DUPS, code=code) self.note('Subclass:', context, offset=ALIGN_OFFSET + OFFSET, code=code) - self.pretty_callable_or_overload(override, context, offset=ALIGN_OFFSET + 2 * OFFSET, - add_class_or_static_decorator=INCLUDE_DECORATOR, - allow_dups=ALLOW_DUPS, code=code) + self.pretty_callable_or_overload( + override, context, offset=ALIGN_OFFSET + 2 * OFFSET, + class_or_static=subtyping_context.override, + allow_dups=ALLOW_DUPS, code=code) def pretty_callable_or_overload(self, - tp: Union[CallableType, Overloaded], + tp: FunctionLike, context: Context, *, + class_or_static: ClassOrStaticContext, offset: int = 0, - add_class_or_static_decorator: bool = False, allow_dups: bool = False, code: Optional[ErrorCode] = None) -> None: if isinstance(tp, CallableType): - if add_class_or_static_decorator: - decorator = pretty_class_or_static_decorator(tp) - if decorator is not None: - self.note(decorator, context, offset=offset, allow_dups=allow_dups, code=code) + decorator = pretty_class_or_static_decorator(tp, class_or_static) + if decorator is not None: + self.note(decorator, context, offset=offset, allow_dups=allow_dups, code=code) self.note(pretty_callable(tp), context, offset=offset, allow_dups=allow_dups, code=code) elif isinstance(tp, Overloaded): self.pretty_overload(tp, context, offset, - add_class_or_static_decorator=add_class_or_static_decorator, + class_or_static=class_or_static, allow_dups=allow_dups, code=code) def argument_incompatible_with_supertype( @@ -1530,14 +1551,14 @@ def pretty_overload(self, context: Context, offset: int, *, - add_class_or_static_decorator: bool = False, + class_or_static: Optional[ClassOrStaticContext] = None, allow_dups: bool = False, code: Optional[ErrorCode] = None) -> None: for item in tp.items: self.note('@overload', context, offset=offset, allow_dups=allow_dups, code=code) - if add_class_or_static_decorator: - decorator = pretty_class_or_static_decorator(item) + if class_or_static is not None: + decorator = pretty_class_or_static_decorator(item, class_or_static) if decorator is not None: self.note(decorator, context, offset=offset, allow_dups=allow_dups, code=code) @@ -1897,13 +1918,21 @@ def format_type_distinctly(*types: Type, bare: bool = False) -> Tuple[str, ...]: return tuple(quote_type_string(s) for s in strs) -def pretty_class_or_static_decorator(tp: CallableType) -> Optional[str]: +def pretty_class_or_static_decorator( + tp: CallableType, context: ClassOrStaticContext, +) -> Optional[str]: """Return @classmethod or @staticmethod, if any, for the given callable type.""" + is_static = context.is_static + is_class = context.is_class if tp.definition is not None and isinstance(tp.definition, SYMBOL_FUNCBASE_TYPES): if tp.definition.is_class: - return '@classmethod' + is_class = True if tp.definition.is_static: - return '@staticmethod' + is_static = True + if is_static: + return '@staticmethod' + if is_class: + return '@classmethod' return None diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 1d2262ab303d1..af176f3f32547 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -5610,3 +5610,67 @@ class C: pass [rechecked] [stale] + +[case testIncrementalClassMethodErrorMessage] +from a import Foo # https://github.com/python/mypy/issues/11791 + +class Bar(Foo): + def foo(cls, arg: int) -> int: + pass +[file a.py] +class Foo: + @classmethod + def foo(cls, arg: int) -> int: + pass +[file a.py.2] +class Foo: + @classmethod + def foo(cls, arg: int) -> int: + pass +[builtins fixtures/classmethod.pyi] +[out] +main:4: error: Signature of "foo" incompatible with supertype "Foo" +main:4: note: Superclass: +main:4: note: @classmethod +main:4: note: def foo(cls, arg: int) -> int +main:4: note: Subclass: +main:4: note: def foo(cls, arg: int) -> int +[out2] +main:4: error: Signature of "foo" incompatible with supertype "Foo" +main:4: note: Superclass: +main:4: note: @classmethod +main:4: note: def foo(cls, arg: int) -> int +main:4: note: Subclass: +main:4: note: def foo(cls, arg: int) -> int + +[case testIncrementalStaticMethodErrorMessage] +from a import Foo + +class Bar(Foo): + def foo(self, arg: int) -> int: + pass +[file a.py] +class Foo: + @staticmethod + def foo(arg: int) -> int: + pass +[file a.py.2] +class Foo: + @staticmethod + def foo(arg: int) -> int: + pass +[builtins fixtures/classmethod.pyi] +[out] +main:4: error: Signature of "foo" incompatible with supertype "Foo" +main:4: note: Superclass: +main:4: note: @staticmethod +main:4: note: def foo(arg: int) -> int +main:4: note: Subclass: +main:4: note: def foo(self, arg: int) -> int +[out2] +main:4: error: Signature of "foo" incompatible with supertype "Foo" +main:4: note: Superclass: +main:4: note: @staticmethod +main:4: note: def foo(arg: int) -> int +main:4: note: Subclass: +main:4: note: def foo(self, arg: int) -> int