Skip to content

Commit 20b891c

Browse files
OddBlokeilevkivskyi
authored andcommitted
Format many-itemed tuples and unions consistently and more clearly (#3893)
When the string representation of a tuple or union is too long to display, a shorter representation is used. This moves from two different representations to <tuple: N items> and <union: N items>, and consistently does not quote them. This also includes a refactoring of the quoting logic in to a separate method, which enables the quoting of star arguments consistently with other types.
1 parent 3ef1e18 commit 20b891c

File tree

3 files changed

+38
-14
lines changed

3 files changed

+38
-14
lines changed

mypy/messages.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ def warn(self, msg: str, context: Context, file: Optional[str] = None,
183183
"""Report a warning message (unless disabled)."""
184184
self.report(msg, context, 'warning', file=file, origin=origin)
185185

186+
def quote_type_string(self, type_string: str) -> str:
187+
"""Quotes a type representation for use in messages."""
188+
no_quote_regex = r'^<(tuple|union): \d+ items>$'
189+
if (type_string in ['Module', 'overloaded function', '<nothing>', '<deleted>']
190+
or re.match(no_quote_regex, type_string) is not None):
191+
# Messages are easier to read if these aren't quoted. We use a
192+
# regex to match strings with variable contents.
193+
return type_string
194+
return '"{}"'.format(type_string)
195+
186196
def format(self, typ: Type, verbosity: int = 0) -> str:
187197
"""
188198
Convert a type to a relatively short string suitable for error messages.
@@ -192,22 +202,16 @@ def format(self, typ: Type, verbosity: int = 0) -> str:
192202
modification of the formatted string is required, callers should use
193203
.format_bare.
194204
"""
195-
ret = self.format_bare(typ, verbosity)
196-
no_quote_regex = r'^tuple\(length \d+\)$'
197-
if (ret in ['Module', 'overloaded function', '<nothing>', '<deleted>']
198-
or re.match(no_quote_regex, ret) is not None):
199-
# Messages are easier to read if these aren't quoted. We use a
200-
# regex to match strings with variable contents.
201-
return ret
202-
return '"{}"'.format(ret)
205+
return self.quote_type_string(self.format_bare(typ, verbosity))
203206

204207
def format_bare(self, typ: Type, verbosity: int = 0) -> str:
205208
"""
206209
Convert a type to a relatively short string suitable for error messages.
207210
208211
This method will return an unquoted string. If a caller doesn't need to
209212
perform post-processing on the string output, .format should be used
210-
instead.
213+
instead. (The caller may want to use .quote_type_string after
214+
processing has happened, to maintain consistent quoting in messages.)
211215
"""
212216
if isinstance(typ, Instance):
213217
itype = typ
@@ -256,7 +260,7 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str:
256260
if len(s) < 400:
257261
return s
258262
else:
259-
return 'tuple(length {})'.format(len(items))
263+
return '<tuple: {} items>'.format(len(items))
260264
elif isinstance(typ, TypedDictType):
261265
# If the TypedDictType is named, return the name
262266
if not typ.is_anonymous():
@@ -284,7 +288,7 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str:
284288
if len(s) < 400:
285289
return s
286290
else:
287-
return 'union type ({} items)'.format(len(items))
291+
return '<union: {} items>'.format(len(items))
288292
elif isinstance(typ, NoneTyp):
289293
return 'None'
290294
elif isinstance(typ, AnyType):
@@ -614,8 +618,9 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
614618
arg_type_str = '*' + arg_type_str
615619
elif arg_kind == ARG_STAR2:
616620
arg_type_str = '**' + arg_type_str
617-
msg = 'Argument {} {}has incompatible type "{}"; expected "{}"'.format(
618-
n, target, arg_type_str, expected_type_str)
621+
msg = 'Argument {} {}has incompatible type {}; expected {}'.format(
622+
n, target, self.quote_type_string(arg_type_str),
623+
self.quote_type_string(expected_type_str))
619624
if isinstance(arg_type, Instance) and isinstance(expected_type, Instance):
620625
notes = append_invariance_notes(notes, arg_type, expected_type)
621626
self.fail(msg, context)

test-data/unit/check-tuples.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ class LongTypeName:
579579
def __add__(self, x: 'LongTypeName') -> 'LongTypeName': pass
580580
[builtins fixtures/tuple.pyi]
581581
[out]
582-
main:3: error: Unsupported operand types for + ("LongTypeName" and tuple(length 50))
582+
main:3: error: Unsupported operand types for + ("LongTypeName" and <tuple: 50 items>)
583583

584584

585585
-- Tuple methods

test-data/unit/check-unions.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,22 @@ if bool():
496496
reveal_type(x) # E: Revealed type is 'Any'
497497
reveal_type(x) # E: Revealed type is 'Union[builtins.int, Any]'
498498
[builtins fixtures/bool.pyi]
499+
500+
[case testLongUnionFormatting]
501+
from typing import Any, Generic, TypeVar, Union
502+
503+
T = TypeVar('T')
504+
505+
class ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes(Generic[T]):
506+
pass
507+
508+
x: Union[ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[int],
509+
ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[object],
510+
ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[float],
511+
ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[str],
512+
ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[Any],
513+
ExtremelyLongTypeNameWhichIsGenericSoWeCanUseItMultipleTimes[bytes]]
514+
515+
def takes_int(arg: int) -> None: pass
516+
517+
takes_int(x) # E: Argument 1 to "takes_int" has incompatible type <union: 6 items>; expected "int"

0 commit comments

Comments
 (0)