Skip to content

Added signature information to incompatible signature failure. #3410

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,7 @@ 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)
name, name_in_super, supertype, node, original, override)

def visit_class_def(self, defn: ClassDef) -> None:
"""Type check a class definition."""
Expand Down
56 changes: 32 additions & 24 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class ErrorInfo:
# Only report this particular messages once per program.
only_once = False

# Do not remove duplicate copies of this message (ignored if only_once is True)
allow_dups = False

# Fine-grained incremental target where this was reported
target = None # type: Optional[str]

Expand All @@ -65,6 +68,7 @@ def __init__(self,
message: str,
blocker: bool,
only_once: bool,
allow_dups: bool,
origin: Tuple[str, int] = None,
target: str = None) -> None:
self.import_ctx = import_ctx
Expand All @@ -78,6 +82,7 @@ def __init__(self,
self.message = message
self.blocker = blocker
self.only_once = only_once
self.allow_dups = allow_dups
self.origin = origin or (file, line)
self.target = target

Expand Down Expand Up @@ -253,7 +258,7 @@ def set_import_context(self, ctx: List[Tuple[str, int]]) -> None:

def report(self, line: int, column: int, message: str, blocker: bool = False,
severity: str = 'error', file: str = None, only_once: bool = False,
origin_line: int = None) -> None:
origin_line: int = None, allow_dups: bool = False) -> None:
"""Report message at the given line using the current error context.

Args:
Expand All @@ -264,6 +269,7 @@ def report(self, line: int, column: int, message: str, blocker: bool = False,
file: if non-None, override current file as context
only_once: if True, only report this exact message once per build
origin_line: if non-None, override current context as origin
allow_dups: if True, don't remove duplicate copies of this (ignored if only_once)
"""
type = self.type_name[-1] # type: Optional[str]
if len(self.function_or_member) > 2:
Expand All @@ -274,7 +280,7 @@ def report(self, line: int, column: int, message: str, blocker: bool = False,
self.function_or_member[-1], line, column, severity, message,
blocker, only_once,
origin=(self.file, origin_line) if origin_line else None,
target=self.current_target())
target=self.current_target(), allow_dups=allow_dups)
self.add_error_info(info)

def add_error_info(self, info: ErrorInfo) -> None:
Expand All @@ -300,7 +306,7 @@ def generate_unused_ignore_notes(self) -> None:
# Don't use report since add_error_info will ignore the error!
info = ErrorInfo(self.import_context(), file, self.current_module(), None,
None, line, -1, 'note', "unused 'type: ignore' comment",
False, False)
False, False, False)
self.error_info.append(info)

def is_typeshed_file(self, file: str) -> bool:
Expand Down Expand Up @@ -338,7 +344,7 @@ def messages(self) -> List[str]:
a = [] # type: List[str]
errors = self.render_messages(self.sort_messages(self.error_info))
errors = self.remove_duplicates(errors)
for file, line, column, severity, message in errors:
for file, line, column, severity, message, allow_dups in errors:
s = ''
if file is not None:
if self.show_column_numbers and line is not None and line >= 0 \
Expand All @@ -363,15 +369,15 @@ def targets(self) -> Set[str]:
if info.target)

def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int,
str, str]]:
str, str, bool]]:
"""Translate the messages into a sequence of tuples.

Each tuple is of form (path, line, col, message. The rendered
sequence includes information about error contexts. The path
item may be None. If the line item is negative, the line
number is not defined for the tuple.
"""
result = [] # type: List[Tuple[Optional[str], int, int, str, str]]
result = [] # type: List[Tuple[Optional[str], int, int, str, str, bool]]
# (path, line, column, severity, message)

prev_import_context = [] # type: List[Tuple[str, int]]
Expand All @@ -397,7 +403,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
# Remove prefix to ignore from path (if present) to
# simplify path.
path = remove_path_prefix(path, self.ignore_prefix)
result.append((None, -1, -1, 'note', fmt.format(path, line)))
result.append((None, -1, -1, 'note', fmt.format(path, line), e.allow_dups))
i -= 1

file = self.simplify_path(e.file)
Expand All @@ -409,27 +415,27 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
e.type != prev_type):
if e.function_or_member is None:
if e.type is None:
result.append((file, -1, -1, 'note', 'At top level:'))
result.append((file, -1, -1, 'note', 'At top level:', e.allow_dups))
else:
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
e.type)))
e.type), e.allow_dups))
else:
if e.type is None:
result.append((file, -1, -1, 'note',
'In function "{}":'.format(
e.function_or_member)))
e.function_or_member), e.allow_dups))
else:
result.append((file, -1, -1, 'note',
'In member "{}" of class "{}":'.format(
e.function_or_member, e.type)))
e.function_or_member, e.type), e.allow_dups))
elif e.type != prev_type:
if e.type is None:
result.append((file, -1, -1, 'note', 'At top level:'))
result.append((file, -1, -1, 'note', 'At top level:', e.allow_dups))
else:
result.append((file, -1, -1, 'note',
'In class "{}":'.format(e.type)))
'In class "{}":'.format(e.type), e.allow_dups))

result.append((file, e.line, e.column, e.severity, e.message))
result.append((file, e.line, e.column, e.severity, e.message, e.allow_dups))

prev_import_context = e.import_ctx
prev_function_or_member = e.function_or_member
Expand Down Expand Up @@ -460,21 +466,23 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]:
result.extend(a)
return result

def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str]]
) -> List[Tuple[Optional[str], int, int, str, str]]:
def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str, bool]]
) -> List[Tuple[Optional[str], int, int, str, str, bool]]:
"""Remove duplicates from a sorted error list."""
res = [] # type: List[Tuple[Optional[str], int, int, str, str]]
res = [] # type: List[Tuple[Optional[str], int, int, str, str, bool]]
i = 0
while i < len(errors):
dup = False
j = i - 1
while (j >= 0 and errors[j][0] == errors[i][0] and
errors[j][1] == errors[i][1]):
if (errors[j][3] == errors[i][3] and
errors[j][4] == errors[i][4]): # ignore column
dup = True
break
j -= 1
# Find duplicates for this error (unless we're allowing duplicates)
if not errors[i][5]:
while (j >= 0 and errors[j][0] == errors[i][0] and
errors[j][1] == errors[i][1]):
if (errors[j][3] == errors[i][3] and
errors[j][4] == errors[i][4]): # ignore column
dup = True
break
j -= 1
if not dup:
res.append(errors[i])
i += 1
Expand Down
56 changes: 43 additions & 13 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import re
import difflib

from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple
from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Optional

from mypy.erasetype import erase_type
from mypy.errors import Errors
Expand Down Expand Up @@ -154,28 +154,34 @@ def is_errors(self) -> bool:
return self.errors.is_errors()

def report(self, msg: str, context: Context, severity: str,
file: str = None, origin: Context = None) -> None:
file: str = None, origin: Context = None, strip: bool = True,
allow_dups: bool = False) -> None:
"""Report an error or note (unless disabled)."""
if self.disable_count <= 0:
self.errors.report(context.get_line() if context else -1,
context.get_column() if context else -1,
msg.strip(), severity=severity, file=file,
origin_line=origin.get_line() if origin else None)
msg.strip() if strip else msg,
severity=severity, file=file,
origin_line=origin.get_line() if origin else None,
allow_dups=allow_dups)

def fail(self, msg: str, context: Context, file: str = None,
origin: Context = None) -> None:
origin: Context = None, strip: bool = True, allow_dups: bool = False) -> None:
"""Report an error message (unless disabled)."""
self.report(msg, context, 'error', file=file, origin=origin)
self.report(msg, context, 'error', file=file, origin=origin, strip=strip,
allow_dups=allow_dups)

def note(self, msg: str, context: Context, file: str = None,
origin: Context = None) -> None:
origin: Context = None, strip: bool = True, allow_dups: bool = False) -> None:
"""Report a note (unless disabled)."""
self.report(msg, context, 'note', file=file, origin=origin)
self.report(msg, context, 'note', file=file, origin=origin, strip=strip,
allow_dups=allow_dups)

def warn(self, msg: str, context: Context, file: str = None,
origin: Context = None) -> None:
origin: Context = None, strip: bool = True, allow_dups: bool = False) -> None:
"""Report a warning message (unless disabled)."""
self.report(msg, context, 'warning', file=file, origin=origin)
self.report(msg, context, 'warning', file=file, origin=origin, strip=strip,
allow_dups=allow_dups)

def format(self, typ: Type, verbosity: int = 0) -> str:
"""Convert a type to a relatively short string that is suitable for error messages.
Expand Down Expand Up @@ -353,6 +359,24 @@ def format_distinctly(self, type1: Type, type2: Type) -> Tuple[str, str]:
return (str1, str2)
return (str1, str2)

def note_signature(self, annotation: str, defn: Optional[FunctionLike], context: Context) -> None:
"""
Given a FunctionLike object, pretty print its signature across multiple lines
and truncate it if there are lots of overloads
"""
if defn is not None:
max_overload_lines = 6 # Print up to 3 overloads, then truncate
pretty_str = defn.pretty_str().split('\n')
# TODO: Add filename and line number to method message
self.note(' {}:'.format(annotation), context, strip=False)
for line in pretty_str[:max_overload_lines]:
self.note(' {}'.format(line), context, strip=False, allow_dups=True)
if len(pretty_str) > max_overload_lines:
# Each overload takes up 2 lines because of "@overload"
additional_overrides = int((len(pretty_str) - max_overload_lines) / 2)
self.note(' <additional {} overloads omitted>'.format(additional_overrides),
context, strip=False, allow_dups=True)

#
# Specific operations
#
Expand Down Expand Up @@ -677,10 +701,16 @@ def incompatible_operator_assignment(self, op: str,

def signature_incompatible_with_supertype(
self, name: str, name_in_super: str, supertype: str,
context: Context) -> None:
context: Context, original: Optional[FunctionLike] = None,
override: Optional[FunctionLike] = None) -> None:
target = self.override_target(name, name_in_super, supertype)
self.fail('Signature of "{}" incompatible with {}'.format(
name, target), context)

# Question: should this be at the top of this file in all caps?
self.fail('Signature of "{}" incompatible with {}'.format(name, target), context)

# Print the original and overridden methods nicely
self.note_signature('Superclass', original, context)
self.note_signature('Subclass', override, context)

def argument_incompatible_with_supertype(
self, arg_num: int, name: str, name_in_supertype: str,
Expand Down
67 changes: 66 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def accept(self, visitor: 'TypeVisitor[T]') -> T:
def __repr__(self) -> str:
return self.accept(TypeStrVisitor())

def pretty_str(self) -> str:
return self.accept(PrettyTypeStrVisitor())

def serialize(self) -> Union[JsonDict, str]:
raise NotImplementedError('Cannot serialize {} instance'.format(self.__class__.__name__))

Expand Down Expand Up @@ -1380,7 +1383,7 @@ def visit_type_type(self, t: TypeType) -> Type:


class TypeStrVisitor(SyntheticTypeVisitor[str]):
"""Visitor for pretty-printing types into strings.
"""Visitor for printing types into human-readable strings.

This is mostly for debugging/testing.

Expand Down Expand Up @@ -1546,6 +1549,68 @@ def keywords_str(self, a: Iterable[Tuple[str, Type]]) -> str:
])


class PrettyTypeStrVisitor(TypeStrVisitor):
"""Overloaded Visitor for pretty-printing types into strings.

Does not preserve original formatting.

Notes:
- Represent unbound types as Foo? or Foo?[...].
- Represent the NoneTyp type as None.
"""
def visit_none_type(self, t: NoneTyp) -> str:
# Note: This isn't discernable from the None value (for readiblity)
return "None"

def visit_overloaded(self, t: Overloaded) -> str:
a = []
for i in t.items():
a.append('@overload\n' + i.accept(self))
return '\n'.join(a)

def visit_callable_type(self, t: CallableType) -> str:
s = ''
bare_asterisk = False
for i in range(len(t.arg_types)):
if len(t.arg_types) > 5:
s += '\n'
if s != '':
s += ', '
if t.arg_kinds[i] in (ARG_NAMED, ARG_NAMED_OPT) and not bare_asterisk:
s += '*, '
bare_asterisk = True
if t.arg_kinds[i] == ARG_STAR:
s += '*'
if t.arg_kinds[i] == ARG_STAR2:
s += '**'
name = t.arg_names[i]
if name:
s += name + ': '
s += t.arg_types[i].accept(self)
if t.arg_kinds[i] in (ARG_OPT, ARG_NAMED_OPT):
s += ' ='

if t.definition is not None and t.definition.name() is not None:
# If we got a "special arg" (i.e: self, cls, etc...), prepend it to the arg list
definition_args = getattr(t.definition, 'arg_names')
if definition_args and t.arg_names != definition_args \
and len(definition_args) > 0:
special_arg = definition_args[0]
if s != '':
s = ', ' + s
s = special_arg + s
s = '{}({})'.format(t.definition.name(), s)
else:
s = '({})'.format(s)

s += ' -> {}'.format(t.ret_type.accept(self))

if t.variables:
s = '{} {}'.format(t.variables, s)

return 'def {}'.format(s)


class TypeQuery(SyntheticTypeVisitor[T]):
"""Visitor for performing queries of types.

Expand Down
8 changes: 6 additions & 2 deletions test-data/unit/check-abstract.test
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,13 @@ class I(metaclass=ABCMeta):
def g(self, x): pass
class A(I):
def f(self, x): pass
def g(self, x, y) -> None: pass \
# E: Signature of "g" incompatible with supertype "I"
def g(self, x, y) -> None: pass
[out]
main:10: error: Signature of "g" incompatible with supertype "I"
main:10: note: Method "g" of "I":
main:10: note: def (x: Any) -> Any
main:10: note: Method "g" of "A":
main:10: note: def g(self, x: Any, y: Any) -> None

[case testAbstractClassWithAllDynamicTypes2]
from abc import abstractmethod, ABCMeta
Expand Down
Loading