From 732d98ecb2a98e4eaea14aba1ed8ac9c1f5ccdb6 Mon Sep 17 00:00:00 2001 From: roberfi Date: Mon, 8 Apr 2024 08:40:02 +0200 Subject: [PATCH] Fix string formatting for string enums (#16555) Fixes #7563 Inside `check_str_format_call` method, it checks if expression of `format` method call is an Enum member and it takes Literal value of that Enum member to check the `format` call arguments, if so. --- mypy/checkexpr.py | 12 +++++++- test-data/unit/check-formatting.test | 42 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 24d8447cdf3e..e8a2e501a452 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -636,7 +636,17 @@ def check_str_format_call(self, e: CallExpr) -> None: if isinstance(e.callee.expr, StrExpr): format_value = e.callee.expr.value elif self.chk.has_type(e.callee.expr): - base_typ = try_getting_literal(self.chk.lookup_type(e.callee.expr)) + typ = get_proper_type(self.chk.lookup_type(e.callee.expr)) + if ( + isinstance(typ, Instance) + and typ.type.is_enum + and isinstance(typ.last_known_value, LiteralType) + and isinstance(typ.last_known_value.value, str) + ): + value_type = typ.type.names[typ.last_known_value.value].type + if isinstance(value_type, Type): + typ = get_proper_type(value_type) + base_typ = try_getting_literal(typ) if isinstance(base_typ, LiteralType) and isinstance(base_typ.value, str): format_value = base_typ.value if format_value is not None: diff --git a/test-data/unit/check-formatting.test b/test-data/unit/check-formatting.test index 75651124b76f..83ae9b526f22 100644 --- a/test-data/unit/check-formatting.test +++ b/test-data/unit/check-formatting.test @@ -588,3 +588,45 @@ class S: '{:%}'.format(0.001) [builtins fixtures/primitives.pyi] [typing fixtures/typing-medium.pyi] + +[case testEnumWithStringToFormatValue] +from enum import Enum + +class Responses(str, Enum): + TEMPLATED = 'insert {} here' + TEMPLATED_WITH_KW = 'insert {value} here' + NORMAL = 'something' + +Responses.TEMPLATED.format(42) +Responses.TEMPLATED_WITH_KW.format(value=42) +Responses.TEMPLATED.format() # E: Cannot find replacement for positional format specifier 0 +Responses.TEMPLATED_WITH_KW.format() # E: Cannot find replacement for named format specifier "value" +Responses.NORMAL.format(42) # E: Not all arguments converted during string formatting +Responses.NORMAL.format(value=42) # E: Not all arguments converted during string formatting +[builtins fixtures/primitives.pyi] + +[case testNonStringEnumToFormatValue] +from enum import Enum + +class Responses(Enum): + TEMPLATED = 'insert {value} here' + +Responses.TEMPLATED.format(value=42) # E: "Responses" has no attribute "format" +[builtins fixtures/primitives.pyi] + +[case testStrEnumWithStringToFormatValue] +# flags: --python-version 3.11 +from enum import StrEnum + +class Responses(StrEnum): + TEMPLATED = 'insert {} here' + TEMPLATED_WITH_KW = 'insert {value} here' + NORMAL = 'something' + +Responses.TEMPLATED.format(42) +Responses.TEMPLATED_WITH_KW.format(value=42) +Responses.TEMPLATED.format() # E: Cannot find replacement for positional format specifier 0 +Responses.TEMPLATED_WITH_KW.format() # E: Cannot find replacement for named format specifier "value" +Responses.NORMAL.format(42) # E: Not all arguments converted during string formatting +Responses.NORMAL.format(value=42) # E: Not all arguments converted during string formatting +[builtins fixtures/primitives.pyi]