Skip to content

Commit bc1dc95

Browse files
Use customized error message for %c interpolation (#10875)
This PR is a follow-up to #10869. The error msg of mypy now is basically the same as CPython. We offer hints for incompatible types.
1 parent b3b3242 commit bc1dc95

File tree

3 files changed

+63
-50
lines changed

3 files changed

+63
-50
lines changed

mypy/checkstrformat.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from mypy.parse import parse
4141

4242
FormatStringExpr = Union[StrExpr, BytesExpr, UnicodeExpr]
43-
Checkers = Tuple[Callable[[Expression], None], Callable[[Type], None]]
43+
Checkers = Tuple[Callable[[Expression], None], Callable[[Type], bool]]
4444
MatchMap = Dict[Tuple[int, int], Match[str]] # span -> match
4545

4646

@@ -98,23 +98,8 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]:
9898
DUMMY_FIELD_NAME: Final = "__dummy_name__"
9999

100100
# Format types supported by str.format() for builtin classes.
101-
SUPPORTED_TYPES_NEW: Final = {
102-
"b",
103-
"c",
104-
"d",
105-
"e",
106-
"E",
107-
"f",
108-
"F",
109-
"g",
110-
"G",
111-
"n",
112-
"o",
113-
"s",
114-
"x",
115-
"X",
116-
"%",
117-
}
101+
SUPPORTED_TYPES_NEW: Final = {"b", "c", "d", "e", "E", "f", "F",
102+
"g", "G", "n", "o", "s", "x", "X", "%"}
118103

119104
# Types that require either int or float.
120105
NUMERIC_TYPES_OLD: Final = {"d", "i", "o", "u", "x", "X", "e", "E", "f", "F", "g", "G"}
@@ -813,22 +798,22 @@ def checkers_for_star(self, context: Context) -> Checkers:
813798
"""
814799
expected = self.named_type('builtins.int')
815800

816-
def check_type(type: Type) -> None:
801+
def check_type(type: Type) -> bool:
817802
expected = self.named_type('builtins.int')
818-
self.chk.check_subtype(type, expected, context, '* wants int',
819-
code=codes.STRING_FORMATTING)
803+
return self.chk.check_subtype(type, expected, context, '* wants int',
804+
code=codes.STRING_FORMATTING)
820805

821806
def check_expr(expr: Expression) -> None:
822807
type = self.accept(expr, expected)
823808
check_type(type)
824809

825810
return check_expr, check_type
826811

827-
def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> None:
828-
self.chk.check_subtype(typ, expected_type, context,
829-
message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
830-
'expression has type', 'placeholder has type',
831-
code=codes.STRING_FORMATTING)
812+
def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> bool:
813+
return self.chk.check_subtype(typ, expected_type, context,
814+
message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
815+
'expression has type', 'placeholder has type',
816+
code=codes.STRING_FORMATTING)
832817

833818
def checkers_for_regular_type(self, type: str,
834819
context: Context,
@@ -840,19 +825,20 @@ def checkers_for_regular_type(self, type: str,
840825
if expected_type is None:
841826
return None
842827

843-
def check_type(typ: Type) -> None:
828+
def check_type(typ: Type) -> bool:
844829
assert expected_type is not None
845-
self.check_placeholder_type(typ, expected_type, context)
846-
if type == 's':
847-
self.check_s_special_cases(expr, typ, context)
830+
ret = self.check_placeholder_type(typ, expected_type, context)
831+
if ret and type == 's':
832+
ret = self.check_s_special_cases(expr, typ, context)
833+
return ret
848834

849835
def check_expr(expr: Expression) -> None:
850836
type = self.accept(expr, expected_type)
851837
check_type(type)
852838

853839
return check_expr, check_type
854840

855-
def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> None:
841+
def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> bool:
856842
"""Additional special cases for %s in bytes vs string context."""
857843
if isinstance(expr, StrExpr):
858844
# Couple special cases for string formatting.
@@ -862,6 +848,7 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
862848
"On Python 3 '%s' % b'abc' produces \"b'abc'\", not 'abc'; "
863849
"use '%r' % b'abc' if this is desired behavior",
864850
context, code=codes.STR_BYTES_PY3)
851+
return False
865852
if self.chk.options.python_version < (3, 0):
866853
if has_type_component(typ, 'builtins.unicode'):
867854
self.unicode_upcast = True
@@ -871,6 +858,8 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
871858
if has_type_component(typ, 'builtins.str'):
872859
self.msg.fail("On Python 3 b'%s' requires bytes, not string", context,
873860
code=codes.STRING_FORMATTING)
861+
return False
862+
return True
874863

875864
def checkers_for_c_type(self, type: str,
876865
context: Context,
@@ -882,20 +871,30 @@ def checkers_for_c_type(self, type: str,
882871
if expected_type is None:
883872
return None
884873

885-
def check_type(type: Type) -> None:
874+
def check_type(type: Type) -> bool:
886875
assert expected_type is not None
887-
self.check_placeholder_type(type, expected_type, context)
876+
if self.chk.options.python_version >= (3, 0) and isinstance(format_expr, BytesExpr):
877+
err_msg = '"%c" requires an integer in range(256) or a single byte'
878+
else:
879+
err_msg = '"%c" requires int or char'
880+
return self.chk.check_subtype(type, expected_type, context, err_msg,
881+
'expression has type',
882+
code=codes.STRING_FORMATTING)
888883

889884
def check_expr(expr: Expression) -> None:
890885
"""int, or str with length 1"""
891886
type = self.accept(expr, expected_type)
892-
# TODO: Use the same the error message when incompatible types match %c
893-
# Python 3 doesn't support b'%c' % str
894-
if not (self.chk.options.python_version >= (3, 0)
895-
and isinstance(format_expr, BytesExpr)):
896-
if isinstance(expr, (StrExpr, BytesExpr)) and len(expr.value) != 1:
887+
# We need further check with expr to make sure that
888+
# it has exact one char or one single byte.
889+
if check_type(type):
890+
# Python 3 doesn't support b'%c' % str
891+
if (self.chk.options.python_version >= (3, 0)
892+
and isinstance(format_expr, BytesExpr)
893+
and isinstance(expr, BytesExpr) and len(expr.value) != 1):
894+
self.msg.requires_int_or_single_byte(context)
895+
# In Python 2, b'%c' is the same as '%c'
896+
elif isinstance(expr, (StrExpr, BytesExpr)) and len(expr.value) != 1:
897897
self.msg.requires_int_or_char(context)
898-
check_type(type)
899898

900899
return check_expr, check_type
901900

mypy/messages.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,12 @@ def string_interpolation_with_star_and_key(self, context: Context) -> None:
961961
self.fail('String interpolation contains both stars and mapping keys', context,
962962
code=codes.STRING_FORMATTING)
963963

964+
def requires_int_or_single_byte(self, context: Context,
965+
format_call: bool = False) -> None:
966+
self.fail('"{}c" requires an integer in range(256) or a single byte'
967+
.format(':' if format_call else '%'),
968+
context, code=codes.STRING_FORMATTING)
969+
964970
def requires_int_or_char(self, context: Context,
965971
format_call: bool = False) -> None:
966972
self.fail('"{}c" requires int or char'.format(':' if format_call else '%'),

test-data/unit/check-expressions.test

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,41 +1281,49 @@ b'%a' % 3
12811281
[case testStringInterPolationCPython2]
12821282
# flags: --py2 --no-strict-optional
12831283
'%c' % 1
1284-
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
1284+
'%c' % 1.0 # E: "%c" requires int or char (expression has type "float")
12851285
'%c' % 's'
1286-
'%c' % '' # E: "%c" requires int or char
1286+
'%c' % '' # E: "%c" requires int or char
12871287
'%c' % 'ab' # E: "%c" requires int or char
12881288
'%c' % b'a'
1289+
'%c' % b'' # E: "%c" requires int or char
1290+
'%c' % b'ab' # E: "%c" requires int or char
12891291
[builtins_py2 fixtures/python2.pyi]
12901292

12911293
[case testStringInterpolationC]
12921294
# flags: --python-version 3.6
12931295
'%c' % 1
1294-
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
1296+
'%c' % 1.0 # E: "%c" requires int or char (expression has type "float")
12951297
'%c' % 's'
1296-
'%c' % '' # E: "%c" requires int or char
1298+
'%c' % '' # E: "%c" requires int or char
12971299
'%c' % 'ab' # E: "%c" requires int or char
1298-
'%c' % b'a' # E: Incompatible types in string interpolation (expression has type "bytes", placeholder has type "Union[int, str]")
1300+
'%c' % b'a' # E: "%c" requires int or char (expression has type "bytes")
1301+
'%c' % b'' # E: "%c" requires int or char (expression has type "bytes")
1302+
'%c' % b'ab' # E: "%c" requires int or char (expression has type "bytes")
12991303
[builtins fixtures/primitives.pyi]
13001304

13011305
[case testBytesInterPolationCPython2]
13021306
# flags: --py2 --no-strict-optional
13031307
b'%c' % 1
1304-
b'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
1308+
b'%c' % 1.0 # E: "%c" requires int or char (expression has type "float")
13051309
b'%c' % 's'
1306-
b'%c' % '' # E: "%c" requires int or char
1310+
b'%c' % '' # E: "%c" requires int or char
13071311
b'%c' % 'ab' # E: "%c" requires int or char
13081312
b'%c' % b'a'
1313+
b'%c' % b'' # E: "%c" requires int or char
1314+
b'%c' % b'aa' # E: "%c" requires int or char
13091315
[builtins_py2 fixtures/python2.pyi]
13101316

13111317
[case testBytesInterpolationC]
13121318
# flags: --python-version 3.6
13131319
b'%c' % 1
1314-
b'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, bytes]")
1315-
b'%c' % 's' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
1316-
b'%c' % '' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
1317-
b'%c' % 'ab' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
1320+
b'%c' % 1.0 # E: "%c" requires an integer in range(256) or a single byte (expression has type "float")
1321+
b'%c' % 's' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str")
1322+
b'%c' % '' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str")
1323+
b'%c' % 'ab' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str")
13181324
b'%c' % b'a'
1325+
b'%c' % b'' # E: "%c" requires an integer in range(256) or a single byte
1326+
b'%c' % b'aa' # E: "%c" requires an integer in range(256) or a single byte
13191327
[builtins fixtures/primitives.pyi]
13201328

13211329
[case testStringInterpolationMappingTypes]

0 commit comments

Comments
 (0)