diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 6c66e48afe391..adf1ba8c071fa 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -40,7 +40,7 @@ from mypy.parse import parse FormatStringExpr = Union[StrExpr, BytesExpr, UnicodeExpr] -Checkers = Tuple[Callable[[Expression], None], Callable[[Type], None]] +Checkers = Tuple[Callable[[Expression], None], Callable[[Type], bool]] MatchMap = Dict[Tuple[int, int], Match[str]] # span -> match @@ -98,23 +98,8 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: DUMMY_FIELD_NAME: Final = "__dummy_name__" # Format types supported by str.format() for builtin classes. -SUPPORTED_TYPES_NEW: Final = { - "b", - "c", - "d", - "e", - "E", - "f", - "F", - "g", - "G", - "n", - "o", - "s", - "x", - "X", - "%", -} +SUPPORTED_TYPES_NEW: Final = {"b", "c", "d", "e", "E", "f", "F", + "g", "G", "n", "o", "s", "x", "X", "%"} # Types that require either int or float. NUMERIC_TYPES_OLD: Final = {"d", "i", "o", "u", "x", "X", "e", "E", "f", "F", "g", "G"} @@ -813,10 +798,10 @@ def checkers_for_star(self, context: Context) -> Checkers: """ expected = self.named_type('builtins.int') - def check_type(type: Type) -> None: + def check_type(type: Type) -> bool: expected = self.named_type('builtins.int') - self.chk.check_subtype(type, expected, context, '* wants int', - code=codes.STRING_FORMATTING) + return self.chk.check_subtype(type, expected, context, '* wants int', + code=codes.STRING_FORMATTING) def check_expr(expr: Expression) -> None: type = self.accept(expr, expected) @@ -824,11 +809,11 @@ def check_expr(expr: Expression) -> None: return check_expr, check_type - def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> None: - self.chk.check_subtype(typ, expected_type, context, - message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION, - 'expression has type', 'placeholder has type', - code=codes.STRING_FORMATTING) + def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> bool: + return self.chk.check_subtype(typ, expected_type, context, + message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION, + 'expression has type', 'placeholder has type', + code=codes.STRING_FORMATTING) def checkers_for_regular_type(self, type: str, context: Context, @@ -840,11 +825,12 @@ def checkers_for_regular_type(self, type: str, if expected_type is None: return None - def check_type(typ: Type) -> None: + def check_type(typ: Type) -> bool: assert expected_type is not None - self.check_placeholder_type(typ, expected_type, context) - if type == 's': - self.check_s_special_cases(expr, typ, context) + ret = self.check_placeholder_type(typ, expected_type, context) + if ret and type == 's': + ret = self.check_s_special_cases(expr, typ, context) + return ret def check_expr(expr: Expression) -> None: type = self.accept(expr, expected_type) @@ -852,7 +838,7 @@ def check_expr(expr: Expression) -> None: return check_expr, check_type - def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> None: + def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> bool: """Additional special cases for %s in bytes vs string context.""" if isinstance(expr, StrExpr): # Couple special cases for string formatting. @@ -862,6 +848,7 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont "On Python 3 '%s' % b'abc' produces \"b'abc'\", not 'abc'; " "use '%r' % b'abc' if this is desired behavior", context, code=codes.STR_BYTES_PY3) + return False if self.chk.options.python_version < (3, 0): if has_type_component(typ, 'builtins.unicode'): self.unicode_upcast = True @@ -871,6 +858,8 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont if has_type_component(typ, 'builtins.str'): self.msg.fail("On Python 3 b'%s' requires bytes, not string", context, code=codes.STRING_FORMATTING) + return False + return True def checkers_for_c_type(self, type: str, context: Context, @@ -882,20 +871,30 @@ def checkers_for_c_type(self, type: str, if expected_type is None: return None - def check_type(type: Type) -> None: + def check_type(type: Type) -> bool: assert expected_type is not None - self.check_placeholder_type(type, expected_type, context) + if self.chk.options.python_version >= (3, 0) and isinstance(format_expr, BytesExpr): + err_msg = '"%c" requires an integer in range(256) or a single byte' + else: + err_msg = '"%c" requires int or char' + return self.chk.check_subtype(type, expected_type, context, err_msg, + 'expression has type', + code=codes.STRING_FORMATTING) def check_expr(expr: Expression) -> None: """int, or str with length 1""" type = self.accept(expr, expected_type) - # 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: + # We need further check with expr to make sure that + # it has exact one char or one single byte. + if check_type(type): + # Python 3 doesn't support b'%c' % str + if (self.chk.options.python_version >= (3, 0) + and isinstance(format_expr, BytesExpr) + and isinstance(expr, BytesExpr) and len(expr.value) != 1): + self.msg.requires_int_or_single_byte(context) + # In Python 2, b'%c' is the same as '%c' + elif isinstance(expr, (StrExpr, BytesExpr)) and len(expr.value) != 1: self.msg.requires_int_or_char(context) - check_type(type) return check_expr, check_type diff --git a/mypy/messages.py b/mypy/messages.py index e1f1f90206635..57198d0386d65 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -961,6 +961,12 @@ def string_interpolation_with_star_and_key(self, context: Context) -> None: self.fail('String interpolation contains both stars and mapping keys', context, code=codes.STRING_FORMATTING) + def requires_int_or_single_byte(self, context: Context, + format_call: bool = False) -> None: + self.fail('"{}c" requires an integer in range(256) or a single byte' + .format(':' if format_call else '%'), + context, code=codes.STRING_FORMATTING) + def requires_int_or_char(self, context: Context, format_call: bool = False) -> None: self.fail('"{}c" requires int or char'.format(':' if format_call else '%'), diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 84727aea8336e..ff3a5efde6adb 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1281,41 +1281,49 @@ b'%a' % 3 [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' % 1.0 # E: "%c" requires int or char (expression has type "float") '%c' % 's' -'%c' % '' # E: "%c" requires int or char +'%c' % '' # E: "%c" requires int or char '%c' % 'ab' # E: "%c" requires int or char '%c' % b'a' +'%c' % b'' # E: "%c" requires int or char +'%c' % b'ab' # E: "%c" requires int or char [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' % 1.0 # E: "%c" requires int or char (expression has type "float") '%c' % 's' -'%c' % '' # E: "%c" requires int or char +'%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]") +'%c' % b'a' # E: "%c" requires int or char (expression has type "bytes") +'%c' % b'' # E: "%c" requires int or char (expression has type "bytes") +'%c' % b'ab' # E: "%c" requires int or char (expression has type "bytes") [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' % 1.0 # E: "%c" requires int or char (expression has type "float") b'%c' % 's' -b'%c' % '' # E: "%c" requires int or char +b'%c' % '' # E: "%c" requires int or char b'%c' % 'ab' # E: "%c" requires int or char b'%c' % b'a' +b'%c' % b'' # E: "%c" requires int or char +b'%c' % b'aa' # E: "%c" requires int or char [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' % 1.0 # E: "%c" requires an integer in range(256) or a single byte (expression has type "float") +b'%c' % 's' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str") +b'%c' % '' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str") +b'%c' % 'ab' # E: "%c" requires an integer in range(256) or a single byte (expression has type "str") b'%c' % b'a' +b'%c' % b'' # E: "%c" requires an integer in range(256) or a single byte +b'%c' % b'aa' # E: "%c" requires an integer in range(256) or a single byte [builtins fixtures/primitives.pyi] [case testStringInterpolationMappingTypes]