diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 78628dc34ba0..6c66e48afe39 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -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] = [] @@ -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 @@ -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 @@ -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 diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 5d3d6b66d7b8..84727aea8336 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -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] @@ -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] diff --git a/test-data/unit/fixtures/python2.pyi b/test-data/unit/fixtures/python2.pyi index 44cb9de9be1d..51af59c8bd45 100644 --- a/test-data/unit/fixtures/python2.pyi +++ b/test-data/unit/fixtures/python2.pyi @@ -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]):