Skip to content

Commit 91c46c7

Browse files
Migrate checkstrformat to use ErrorMessage class
1 parent 9ce3470 commit 91c46c7

File tree

4 files changed

+131
-95
lines changed

4 files changed

+131
-95
lines changed

mypy/checkstrformat.py

+25-91
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,11 @@ def parse_format_value(
199199
custom_match, start_pos=start_pos, non_standard_format_spec=True
200200
)
201201
else:
202-
msg.fail(
203-
"Invalid conversion specifier in format string",
204-
ctx,
205-
code=codes.STRING_FORMATTING,
206-
)
202+
msg.fail(message_registry.FORMAT_STR_INVALID_SPECIFIER, ctx)
207203
return None
208204

209205
if conv_spec.key and ("{" in conv_spec.key or "}" in conv_spec.key):
210-
msg.fail("Conversion value must not contain { or }", ctx, code=codes.STRING_FORMATTING)
206+
msg.fail(message_registry.FORMAT_STR_BRACES_IN_SPECIFIER, ctx)
211207
return None
212208
result.append(conv_spec)
213209

@@ -218,11 +214,7 @@ def parse_format_value(
218214
and ("{" in conv_spec.format_spec or "}" in conv_spec.format_spec)
219215
):
220216
if nested:
221-
msg.fail(
222-
"Formatting nesting must be at most two levels deep",
223-
ctx,
224-
code=codes.STRING_FORMATTING,
225-
)
217+
msg.fail(message_registry.FORMAT_STR_NESTING_ATMOST_TWO_LEVELS, ctx)
226218
return None
227219
sub_conv_specs = parse_format_value(conv_spec.format_spec, ctx, msg, nested=True)
228220
if sub_conv_specs is None:
@@ -260,11 +252,7 @@ def find_non_escaped_targets(
260252
if pos < len(format_value) - 1 and format_value[pos + 1] == "}":
261253
pos += 1
262254
else:
263-
msg.fail(
264-
"Invalid conversion specifier in format string: unexpected }",
265-
ctx,
266-
code=codes.STRING_FORMATTING,
267-
)
255+
msg.fail(message_registry.FORMAT_STR_UNEXPECTED_RBRACE, ctx)
268256
return None
269257
else:
270258
# Adjust nesting level, then either continue adding chars or move on.
@@ -279,11 +267,7 @@ def find_non_escaped_targets(
279267
next_spec = ""
280268
pos += 1
281269
if nesting:
282-
msg.fail(
283-
"Invalid conversion specifier in format string: unmatched {",
284-
ctx,
285-
code=codes.STRING_FORMATTING,
286-
)
270+
msg.fail(message_registry.FORMAT_STR_UNMATCHED_LBRACE, ctx)
287271
return None
288272
return result
289273

@@ -369,9 +353,8 @@ def check_specs_in_format_call(
369353
):
370354
# TODO: add support for some custom specs like datetime?
371355
self.msg.fail(
372-
"Unrecognized format" ' specification "{}"'.format(spec.format_spec[1:]),
356+
message_registry.UNRECOGNIZED_FORMAT_SPEC.format(spec.format_spec[1:]),
373357
call,
374-
code=codes.STRING_FORMATTING,
375358
)
376359
continue
377360
# Adjust expected and actual types.
@@ -390,10 +373,10 @@ def check_specs_in_format_call(
390373
# If the explicit conversion is given, then explicit conversion is called _first_.
391374
if spec.conversion[1] not in "rsa":
392375
self.msg.fail(
393-
'Invalid conversion type "{}",'
394-
' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
376+
message_registry.FORMAT_STR_INVALID_CONVERSION_TYPE.format(
377+
spec.conversion[1]
378+
),
395379
call,
396-
code=codes.STRING_FORMATTING,
397380
)
398381
actual_type = self.named_type("builtins.str")
399382

@@ -433,13 +416,7 @@ def perform_special_format_checks(
433416
if has_type_component(actual_type, "builtins.bytes") and not custom_special_method(
434417
actual_type, "__str__"
435418
):
436-
self.msg.fail(
437-
'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", '
438-
'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). '
439-
"Otherwise, decode the bytes",
440-
call,
441-
code=codes.STR_BYTES_PY3,
442-
)
419+
self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR, call)
443420
if spec.flags:
444421
numeric_types = UnionType(
445422
[self.named_type("builtins.int"), self.named_type("builtins.float")]
@@ -451,11 +428,7 @@ def perform_special_format_checks(
451428
and not is_subtype(actual_type, numeric_types)
452429
and not custom_special_method(actual_type, "__format__")
453430
):
454-
self.msg.fail(
455-
"Numeric flags are only allowed for numeric types",
456-
call,
457-
code=codes.STRING_FORMATTING,
458-
)
431+
self.msg.fail(message_registry.FORMAT_STR_INVALID_NUMERIC_FLAG, call)
459432

460433
def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]:
461434
"""Find replacement expression for every specifier in str.format() call.
@@ -469,19 +442,14 @@ def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Exp
469442
expr = self.get_expr_by_position(int(key), call)
470443
if not expr:
471444
self.msg.fail(
472-
"Cannot find replacement for positional"
473-
" format specifier {}".format(key),
474-
call,
475-
code=codes.STRING_FORMATTING,
445+
message_registry.FORMAT_STR_REPLACEMENT_NOT_FOUND.format(key), call
476446
)
477447
expr = TempNode(AnyType(TypeOfAny.from_error))
478448
else:
479449
expr = self.get_expr_by_name(key, call)
480450
if not expr:
481451
self.msg.fail(
482-
"Cannot find replacement for named" ' format specifier "{}"'.format(key),
483-
call,
484-
code=codes.STRING_FORMATTING,
452+
message_registry.FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND.format(key), call
485453
)
486454
expr = TempNode(AnyType(TypeOfAny.from_error))
487455
result.append(expr)
@@ -555,11 +523,7 @@ def auto_generate_keys(self, all_specs: list[ConversionSpecifier], ctx: Context)
555523
some_defined = any(s.key and s.key.isdecimal() for s in all_specs)
556524
all_defined = all(bool(s.key) for s in all_specs)
557525
if some_defined and not all_defined:
558-
self.msg.fail(
559-
"Cannot combine automatic field numbering and manual field specification",
560-
ctx,
561-
code=codes.STRING_FORMATTING,
562-
)
526+
self.msg.fail(message_registry.FORMAT_STR_PARTIAL_FIELD_NUMBERING, ctx)
563527
return False
564528
if all_defined:
565529
return True
@@ -594,11 +558,7 @@ def apply_field_accessors(
594558
dummy, fnam="<format>", module=None, options=self.chk.options, errors=temp_errors
595559
)
596560
if temp_errors.is_errors():
597-
self.msg.fail(
598-
f'Syntax error in format specifier "{spec.field}"',
599-
ctx,
600-
code=codes.STRING_FORMATTING,
601-
)
561+
self.msg.fail(message_registry.FORMAT_STR_SYNTAX_ERROR.format(spec.field), ctx)
602562
return TempNode(AnyType(TypeOfAny.from_error))
603563

604564
# These asserts are guaranteed by the original regexp.
@@ -637,10 +597,7 @@ class User(TypedDict):
637597
"""
638598
if not isinstance(temp_ast, (MemberExpr, IndexExpr)):
639599
self.msg.fail(
640-
"Only index and member expressions are allowed in"
641-
' format field accessors; got "{}"'.format(spec.field),
642-
ctx,
643-
code=codes.STRING_FORMATTING,
600+
message_registry.FORMAT_STR_INVALID_ACCESSOR_EXPR.format(spec.field), ctx
644601
)
645602
return False
646603
if isinstance(temp_ast, MemberExpr):
@@ -651,10 +608,10 @@ class User(TypedDict):
651608
assert spec.key, "Call this method only after auto-generating keys!"
652609
assert spec.field
653610
self.msg.fail(
654-
"Invalid index expression in format field"
655-
' accessor "{}"'.format(spec.field[len(spec.key) :]),
611+
message_registry.FORMAT_STR_INVALID_INDEX_ACCESSOR.format(
612+
spec.field[len(spec.key) :]
613+
),
656614
ctx,
657-
code=codes.STRING_FORMATTING,
658615
)
659616
return False
660617
if isinstance(temp_ast.index, NameExpr):
@@ -683,11 +640,7 @@ def check_str_interpolation(self, expr: FormatStringExpr, replacements: Expressi
683640
specifiers = parse_conversion_specifiers(expr.value)
684641
has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr)
685642
if isinstance(expr, BytesExpr) and self.chk.options.python_version < (3, 5):
686-
self.msg.fail(
687-
"Bytes formatting is only supported in Python 3.5 and later",
688-
replacements,
689-
code=codes.STRING_FORMATTING,
690-
)
643+
self.msg.fail(message_registry.FORMAT_STR_BYTES_ABOVE_PY35, replacements)
691644
return AnyType(TypeOfAny.from_error)
692645

693646
if has_mapping_keys is None:
@@ -794,9 +747,7 @@ def check_mapping_str_interpolation(
794747
# Special case: for bytes formatting keys must be bytes.
795748
if not isinstance(k, BytesExpr):
796749
self.msg.fail(
797-
"Dictionary keys in bytes formatting must be bytes, not strings",
798-
expr,
799-
code=codes.STRING_FORMATTING,
750+
message_registry.FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES, expr
800751
)
801752
key_str = cast(FormatStringExpr, k).value
802753
mapping[key_str] = self.accept(v)
@@ -948,21 +899,12 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
948899
if isinstance(expr, StrExpr):
949900
# Couple special cases for string formatting.
950901
if has_type_component(typ, "builtins.bytes"):
951-
self.msg.fail(
952-
'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". '
953-
'If this is desired behavior use "%r" % x. Otherwise, decode the bytes',
954-
context,
955-
code=codes.STR_BYTES_PY3,
956-
)
902+
self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR_OLD, context)
957903
return False
958904
if isinstance(expr, BytesExpr):
959905
# A special case for bytes formatting: b'%s' actually requires bytes on Python 3.
960906
if has_type_component(typ, "builtins.str"):
961-
self.msg.fail(
962-
"On Python 3 b'%s' requires bytes, not string",
963-
context,
964-
code=codes.STRING_FORMATTING,
965-
)
907+
self.msg.fail(message_registry.FORMAT_STR_BYTES_REQUIRED_PY3, context)
966908
return False
967909
return True
968910

@@ -1024,18 +966,10 @@ def conversion_type(
1024966
INT_TYPES = REQUIRE_INT_NEW if format_call else REQUIRE_INT_OLD
1025967
if p == "b" and not format_call:
1026968
if self.chk.options.python_version < (3, 5):
1027-
self.msg.fail(
1028-
'Format character "b" is only supported in Python 3.5 and later',
1029-
context,
1030-
code=codes.STRING_FORMATTING,
1031-
)
969+
self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35, context)
1032970
return None
1033971
if not isinstance(expr, BytesExpr):
1034-
self.msg.fail(
1035-
'Format character "b" is only supported on bytes patterns',
1036-
context,
1037-
code=codes.STRING_FORMATTING,
1038-
)
972+
self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER, context)
1039973
return None
1040974
return self.named_type("builtins.bytes")
1041975
elif p == "a":

mypy/errorcodes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ def __str__(self) -> str:
109109
VALID_NEWTYPE: Final = ErrorCode(
110110
"valid-newtype", "Check that argument 2 to NewType is valid", "General"
111111
)
112-
STRING_FORMATTING: Final = ErrorCode(
112+
STRING_FORMATTING: Final[ErrorCode] = ErrorCode(
113113
"str-format", "Check that string formatting/interpolation is type-safe", "General"
114114
)
115-
STR_BYTES_PY3: Final = ErrorCode(
115+
STR_BYTES_PY3: Final[ErrorCode] = ErrorCode(
116116
"str-bytes-safe", "Warn about implicit coercions related to bytes and string types", "General"
117117
)
118118
EXIT_RETURN: Final = ErrorCode(

mypy/message_registry.py

+71
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,74 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
313313
ARG_NAME_EXPECTED_STRING_LITERAL: Final = ErrorMessage(
314314
"Expected string literal for argument name, got {}", codes.SYNTAX
315315
)
316+
317+
FORMAT_STR_INVALID_SPECIFIER: Final = ErrorMessage(
318+
"Invalid conversion specifier in format string", codes.STRING_FORMATTING
319+
)
320+
FORMAT_STR_BRACES_IN_SPECIFIER: Final = ErrorMessage(
321+
"Conversion value must not contain { or }", codes.STRING_FORMATTING
322+
)
323+
FORMAT_STR_NESTING_ATMOST_TWO_LEVELS: Final = ErrorMessage(
324+
"Formatting nesting must be at most two levels deep", codes.STRING_FORMATTING
325+
)
326+
FORMAT_STR_UNEXPECTED_RBRACE: Final = ErrorMessage(
327+
"Invalid conversion specifier in format string: unexpected }", codes.STRING_FORMATTING
328+
)
329+
FORMAT_STR_UNMATCHED_LBRACE: Final = ErrorMessage(
330+
"Invalid conversion specifier in format string: unmatched {", codes.STRING_FORMATTING
331+
)
332+
UNRECOGNIZED_FORMAT_SPEC: Final = ErrorMessage(
333+
'Unrecognized format specification "{}"', codes.STRING_FORMATTING
334+
)
335+
FORMAT_STR_INVALID_CONVERSION_TYPE: Final = ErrorMessage(
336+
'Invalid conversion type "{}", must be one of "r", "s" or "a"', codes.STRING_FORMATTING
337+
)
338+
FORMAT_STR_BYTES_USE_REPR: Final = ErrorMessage(
339+
'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", '
340+
'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). '
341+
"Otherwise, decode the bytes",
342+
codes.STR_BYTES_PY3,
343+
)
344+
FORMAT_STR_BYTES_USE_REPR_OLD: Final = ErrorMessage(
345+
'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". '
346+
'If this is desired behavior use "%r" % x. Otherwise, decode the bytes',
347+
codes.STR_BYTES_PY3,
348+
)
349+
FORMAT_STR_INVALID_NUMERIC_FLAG: Final = ErrorMessage(
350+
"Numeric flags are only allowed for numeric types", codes.STRING_FORMATTING
351+
)
352+
FORMAT_STR_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
353+
"Cannot find replacement for positional format specifier {}", codes.STRING_FORMATTING
354+
)
355+
FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
356+
'Cannot find replacement for named format specifier "{}"', codes.STRING_FORMATTING
357+
)
358+
FORMAT_STR_PARTIAL_FIELD_NUMBERING: Final = ErrorMessage(
359+
"Cannot combine automatic field numbering and manual field specification",
360+
codes.STRING_FORMATTING,
361+
)
362+
FORMAT_STR_SYNTAX_ERROR: Final = ErrorMessage(
363+
'Syntax error in format specifier "{}"', codes.STRING_FORMATTING
364+
)
365+
FORMAT_STR_INVALID_ACCESSOR_EXPR: Final = ErrorMessage(
366+
'Only index and member expressions are allowed in format field accessors; got "{}"',
367+
codes.STRING_FORMATTING,
368+
)
369+
FORMAT_STR_INVALID_INDEX_ACCESSOR: Final = ErrorMessage(
370+
'Invalid index expression in format field accessor "{}"', codes.STRING_FORMATTING
371+
)
372+
FORMAT_STR_BYTES_ABOVE_PY35: Final = ErrorMessage(
373+
"Bytes formatting is only supported in Python 3.5 and later", codes.STRING_FORMATTING
374+
)
375+
FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES: Final = ErrorMessage(
376+
"Dictionary keys in bytes formatting must be bytes, not strings", codes.STRING_FORMATTING
377+
)
378+
FORMAT_STR_BYTES_REQUIRED_PY3: Final = ErrorMessage(
379+
"On Python 3 b'%s' requires bytes, not string", codes.STRING_FORMATTING
380+
)
381+
FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35: Final = ErrorMessage(
382+
'Format character "b" is only supported in Python 3.5 and later', codes.STRING_FORMATTING
383+
)
384+
FORMAT_STR_INVALID_BYTES_SPECIFIER: Final = ErrorMessage(
385+
'Format character "b" is only supported on bytes patterns', codes.STRING_FORMATTING
386+
)

mypy/messages.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import re
1717
from contextlib import contextmanager
1818
from textwrap import dedent
19-
from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast
19+
from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast, overload
2020
from typing_extensions import Final
2121

2222
import mypy.typeops
@@ -261,6 +261,7 @@ def span_from_context(ctx: Context) -> Iterable[int]:
261261
allow_dups=allow_dups,
262262
)
263263

264+
@overload
264265
def fail(
265266
self,
266267
msg: str,
@@ -270,10 +271,40 @@ def fail(
270271
file: str | None = None,
271272
allow_dups: bool = False,
272273
secondary_context: Context | None = None,
274+
) -> None:
275+
...
276+
277+
@overload
278+
def fail(
279+
self,
280+
msg: message_registry.ErrorMessage,
281+
context: Context | None,
282+
*,
283+
file: str | None = None,
284+
allow_dups: bool = False,
285+
secondary_context: Context | None = None,
286+
) -> None:
287+
...
288+
289+
def fail(
290+
self,
291+
msg: str | message_registry.ErrorMessage,
292+
context: Context | None,
293+
*,
294+
code: ErrorCode | None = None,
295+
file: str | None = None,
296+
allow_dups: bool = False,
297+
secondary_context: Context | None = None,
273298
) -> None:
274299
"""Report an error message (unless disabled)."""
300+
if isinstance(msg, message_registry.ErrorMessage):
301+
msg_str = msg.value
302+
code = msg.code
303+
else:
304+
msg_str = msg
305+
275306
self.report(
276-
msg,
307+
msg_str,
277308
context,
278309
"error",
279310
code=code,

0 commit comments

Comments
 (0)