Skip to content

Commit

Permalink
Fix %c string and bytes interpolation (#10869)
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
97littleleaf11 authored Jul 25, 2021
1 parent 524c924 commit b3b3242
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 9 deletions.
23 changes: 15 additions & 8 deletions mypy/checkstrformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ def build_replacement_checkers(self, specifiers: List[ConversionSpecifier],
def replacement_checkers(self, specifier: ConversionSpecifier, context: Context,
expr: FormatStringExpr) -> Optional[List[Checkers]]:
"""Returns a list of tuples of two functions that check whether a replacement is
of the right type for the specifier. The first functions take a node and checks
of the right type for the specifier. The first function takes a node and checks
its type in the right type context. The second function just checks a type.
"""
checkers: List[Checkers] = []
Expand Down Expand Up @@ -874,11 +874,11 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont

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

Expand All @@ -889,8 +889,12 @@ def check_type(type: Type) -> None:
def check_expr(expr: Expression) -> None:
"""int, or str with length 1"""
type = self.accept(expr, expected_type)
if isinstance(expr, (StrExpr, BytesExpr)) and len(cast(StrExpr, expr).value) != 1:
self.msg.requires_int_or_char(context)
# TODO: Use the same the error message when incompatible types match %c
# Python 3 doesn't support b'%c' % str
if not (self.chk.options.python_version >= (3, 0)
and isinstance(format_expr, BytesExpr)):
if isinstance(expr, (StrExpr, BytesExpr)) and len(expr.value) != 1:
self.msg.requires_int_or_char(context)
check_type(type)

return check_expr, check_type
Expand Down Expand Up @@ -939,9 +943,12 @@ def conversion_type(self, p: str, context: Context, expr: FormatStringExpr,
numeric_types.append(self.named_type('typing.SupportsInt'))
return UnionType.make_union(numeric_types)
elif p in ['c']:
return UnionType([self.named_type('builtins.int'),
self.named_type('builtins.float'),
self.named_type('builtins.str')])
if isinstance(expr, BytesExpr):
return UnionType([self.named_type('builtins.int'),
self.named_type('builtins.bytes')])
else:
return UnionType([self.named_type('builtins.int'),
self.named_type('builtins.str')])
else:
self.msg.unsupported_placeholder(p, context)
return None
Expand Down
35 changes: 34 additions & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1278,11 +1278,44 @@ b'%a' % 3
[builtins fixtures/primitives.pyi]
[typing fixtures/typing-medium.pyi]

[case testStringInterPolationCPython2]
# flags: --py2 --no-strict-optional
'%c' % 1
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
'%c' % 's'
'%c' % '' # E: "%c" requires int or char
'%c' % 'ab' # E: "%c" requires int or char
'%c' % b'a'
[builtins_py2 fixtures/python2.pyi]

[case testStringInterpolationC]
# flags: --python-version 3.6
'%c' % 1
'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
'%c' % 's'
'%c' % '' # E: "%c" requires int or char
'%c' % 'ab' # E: "%c" requires int or char
'%c' % b'a' # E: Incompatible types in string interpolation (expression has type "bytes", placeholder has type "Union[int, str]")
[builtins fixtures/primitives.pyi]

[case testBytesInterPolationCPython2]
# flags: --py2 --no-strict-optional
b'%c' % 1
b'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, str]")
b'%c' % 's'
b'%c' % '' # E: "%c" requires int or char
b'%c' % 'ab' # E: "%c" requires int or char
b'%c' % b'a'
[builtins_py2 fixtures/python2.pyi]

[case testBytesInterpolationC]
# flags: --python-version 3.6
b'%c' % 1
b'%c' % 1.0 # E: Incompatible types in string interpolation (expression has type "float", placeholder has type "Union[int, bytes]")
b'%c' % 's' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
b'%c' % '' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
b'%c' % 'ab' # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, bytes]")
b'%c' % b'a'
[builtins fixtures/primitives.pyi]

[case testStringInterpolationMappingTypes]
Expand Down Expand Up @@ -1540,7 +1573,7 @@ x: Union[Good, Bad]

class C:
...
'{:c}'.format(C()) # E: Incompatible types in string interpolation (expression has type "C", placeholder has type "Union[int, float, str]")
'{:c}'.format(C()) # E: Incompatible types in string interpolation (expression has type "C", placeholder has type "Union[int, str]")
x: str
'{:c}'.format(x)
[builtins fixtures/primitives.pyi]
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/fixtures/python2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class unicode:
def format(self, *args, **kwars) -> unicode: ...
class bool(int): pass

bytes = str

T = TypeVar('T')
S = TypeVar('S')
class list(Iterable[T], Generic[T]):
Expand Down

0 comments on commit b3b3242

Please sign in to comment.