Skip to content

Fix %c string and bytes interpolation #10869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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