Skip to content

Commit b3b3242

Browse files
Fix %c string and bytes interpolation (#10869)
According to [PEP461](https://www.python.org/dev/peps/pep-0461/), conversion type %c performs differently for string and bytes. In addition, `b'%c’ % str` is not supported in Python3. ## Test Plan CPython Python3: ```py ss = 'a' bb = b'a' ii = 97 ff = 97.0 print('%c' % ss) # print('%c' % '') # TypeError: %c requires int or char # print('%c' % 'aa') # TypeError: %c requires int or char # print('%c' % bb) # TypeError: %c requires int or char print('%c' % ii) # print('%c' % ff) # TypeError: %c requires int or char # print(b'%c' % ss) # TypeError: %c requires an integer in range(256) or a single byte # print(b'%c' % '') # TypeError: %c requires an integer in range(256) or a single byte # print(b'%c' % 'aa') # TypeError: %c requires an integer in range(256) or a single byte print(b'%c' % bb) print(b'%c' % ii) # print(b'%c' % ff) # TypeError: %c requires an integer in range(256) or a single byte ``` CPython Python2: ```py print('%c' % ss) # print('%c' % '') # TypeError: %c requires int or char # print('%c' % 'aa') # TypeError: %c requires int or char print('%c' % bb) print('%c' % ii) # print('%c' % ff) # TypeError: integer argument expected, got float print(b'%c' % ss) # print('%c' % '') # TypeError: %c requires int or char # print('%c' % 'aa') # TypeError: %c requires int or char print(b'%c' % bb) print(b'%c' % ii) # print(b'%c' % ff) # TypeError: integer argument expected, got float ```
1 parent 524c924 commit b3b3242

File tree

3 files changed

+51
-9
lines changed

3 files changed

+51
-9
lines changed

mypy/checkstrformat.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ def build_replacement_checkers(self, specifiers: List[ConversionSpecifier],
786786
def replacement_checkers(self, specifier: ConversionSpecifier, context: Context,
787787
expr: FormatStringExpr) -> Optional[List[Checkers]]:
788788
"""Returns a list of tuples of two functions that check whether a replacement is
789-
of the right type for the specifier. The first functions take a node and checks
789+
of the right type for the specifier. The first function takes a node and checks
790790
its type in the right type context. The second function just checks a type.
791791
"""
792792
checkers: List[Checkers] = []
@@ -874,11 +874,11 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
874874

875875
def checkers_for_c_type(self, type: str,
876876
context: Context,
877-
expr: FormatStringExpr) -> Optional[Checkers]:
877+
format_expr: FormatStringExpr) -> Optional[Checkers]:
878878
"""Returns a tuple of check functions that check whether, respectively,
879879
a node or a type is compatible with 'type' that is a character type.
880880
"""
881-
expected_type = self.conversion_type(type, context, expr)
881+
expected_type = self.conversion_type(type, context, format_expr)
882882
if expected_type is None:
883883
return None
884884

@@ -889,8 +889,12 @@ def check_type(type: Type) -> None:
889889
def check_expr(expr: Expression) -> None:
890890
"""int, or str with length 1"""
891891
type = self.accept(expr, expected_type)
892-
if isinstance(expr, (StrExpr, BytesExpr)) and len(cast(StrExpr, expr).value) != 1:
893-
self.msg.requires_int_or_char(context)
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:
897+
self.msg.requires_int_or_char(context)
894898
check_type(type)
895899

896900
return check_expr, check_type
@@ -939,9 +943,12 @@ def conversion_type(self, p: str, context: Context, expr: FormatStringExpr,
939943
numeric_types.append(self.named_type('typing.SupportsInt'))
940944
return UnionType.make_union(numeric_types)
941945
elif p in ['c']:
942-
return UnionType([self.named_type('builtins.int'),
943-
self.named_type('builtins.float'),
944-
self.named_type('builtins.str')])
946+
if isinstance(expr, BytesExpr):
947+
return UnionType([self.named_type('builtins.int'),
948+
self.named_type('builtins.bytes')])
949+
else:
950+
return UnionType([self.named_type('builtins.int'),
951+
self.named_type('builtins.str')])
945952
else:
946953
self.msg.unsupported_placeholder(p, context)
947954
return None

test-data/unit/check-expressions.test

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,11 +1278,44 @@ b'%a' % 3
12781278
[builtins fixtures/primitives.pyi]
12791279
[typing fixtures/typing-medium.pyi]
12801280

1281+
[case testStringInterPolationCPython2]
1282+
# flags: --py2 --no-strict-optional
1283+
'%c' % 1
1284+
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
1285+
'%c' % 's'
1286+
'%c' % '' # E: "%c" requires int or char
1287+
'%c' % 'ab' # E: "%c" requires int or char
1288+
'%c' % b'a'
1289+
[builtins_py2 fixtures/python2.pyi]
1290+
12811291
[case testStringInterpolationC]
1292+
# flags: --python-version 3.6
12821293
'%c' % 1
1294+
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
12831295
'%c' % 's'
12841296
'%c' % '' # E: "%c" requires int or char
12851297
'%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]")
1299+
[builtins fixtures/primitives.pyi]
1300+
1301+
[case testBytesInterPolationCPython2]
1302+
# flags: --py2 --no-strict-optional
1303+
b'%c' % 1
1304+
b'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
1305+
b'%c' % 's'
1306+
b'%c' % '' # E: "%c" requires int or char
1307+
b'%c' % 'ab' # E: "%c" requires int or char
1308+
b'%c' % b'a'
1309+
[builtins_py2 fixtures/python2.pyi]
1310+
1311+
[case testBytesInterpolationC]
1312+
# flags: --python-version 3.6
1313+
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]")
1318+
b'%c' % b'a'
12861319
[builtins fixtures/primitives.pyi]
12871320

12881321
[case testStringInterpolationMappingTypes]
@@ -1540,7 +1573,7 @@ x: Union[Good, Bad]
15401573

15411574
class C:
15421575
...
1543-
'{:c}'.format(C()) # E: Incompatible types in string interpolation (expression has type "C", placeholder has type "Union[int, float, str]")
1576+
'{:c}'.format(C()) # E: Incompatible types in string interpolation (expression has type "C", placeholder has type "Union[int, str]")
15441577
x: str
15451578
'{:c}'.format(x)
15461579
[builtins fixtures/primitives.pyi]

test-data/unit/fixtures/python2.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class unicode:
1818
def format(self, *args, **kwars) -> unicode: ...
1919
class bool(int): pass
2020

21+
bytes = str
22+
2123
T = TypeVar('T')
2224
S = TypeVar('S')
2325
class list(Iterable[T], Generic[T]):

0 commit comments

Comments
 (0)