From 3fe1aa7999c7aa4d49c62a40b6577261a97631f1 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:07:18 -0400 Subject: [PATCH 01/13] Check for None type in format call --- mypy/checkstrformat.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 45075bd37552..e0ef8daa99ef 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -52,6 +52,7 @@ AnyType, Instance, LiteralType, + NoneType, TupleType, Type, TypeOfAny, @@ -98,9 +99,10 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: # Conversion (optional) is ! followed by one of letters for forced repr(), str(), or ascii(). conversion = r"(?P![^:])?" - + print("in compile new format") # Format specification (optional) follows its own mini-language: if not custom_spec: + print("not custom") # Fill and align is valid for all builtin types. fill_align = r"(?P.?[<>=^])?" # Number formatting options are only valid for int, float, complex, and Decimal, @@ -111,8 +113,10 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: conv_type = r"(?P.)?" # only some are supported, but we want to give a better error format_spec = r"(?P:" + fill_align + num_spec + conv_type + r")?" else: + print("custom") # Custom types can define their own form_spec using __format__(). format_spec = r"(?P:.*)?" + print() return re.compile(field + conversion + format_spec) @@ -182,7 +186,12 @@ def parse_format_value( The specifiers may be nested (two levels maximum), in this case they are ordered as '{0:{1}}, {2:{3}{4}}'. Return None in case of an error. """ + print("in parse") + print("format_value", format_value) + print("ctx", ctx) + print("msg", msg) top_targets = find_non_escaped_targets(format_value, ctx, msg) + print("top_targets", top_targets) if top_targets is None: return None @@ -324,7 +333,12 @@ def check_str_format_call(self, call: CallExpr, format_value: str) -> None: - 's' must not accept bytes - non-empty flags are only allowed for numeric types """ + print("in check str formal") + print("Call: ", call) + print("format_value: ", format_value) conv_specs = parse_format_value(format_value, call, self.msg) + # print("conv_specs", conv_specs) + print() if conv_specs is None: return if not self.auto_generate_keys(conv_specs, call): @@ -338,6 +352,7 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ + print("in check specs") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) @@ -399,6 +414,9 @@ def check_specs_in_format_call( get_proper_types(a_type.items) if isinstance(a_type, UnionType) else [a_type] ) for a_type in actual_items: + print("atype", a_type, type(a_type)) + if isinstance(a_type, NoneType): + print("isNone") if custom_special_method(a_type, "__format__"): continue self.check_placeholder_type(a_type, expected_type, call) From 4180b553b371dd12840d2995e40541b5930dc535 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:04:19 -0400 Subject: [PATCH 02/13] Add check for alignment specifier on none --- mypy/checkstrformat.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index e0ef8daa99ef..d8af510e59ef 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -99,10 +99,8 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: # Conversion (optional) is ! followed by one of letters for forced repr(), str(), or ascii(). conversion = r"(?P![^:])?" - print("in compile new format") # Format specification (optional) follows its own mini-language: if not custom_spec: - print("not custom") # Fill and align is valid for all builtin types. fill_align = r"(?P.?[<>=^])?" # Number formatting options are only valid for int, float, complex, and Decimal, @@ -113,11 +111,8 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: conv_type = r"(?P.)?" # only some are supported, but we want to give a better error format_spec = r"(?P:" + fill_align + num_spec + conv_type + r")?" else: - print("custom") # Custom types can define their own form_spec using __format__(). format_spec = r"(?P:.*)?" - print() - return re.compile(field + conversion + format_spec) @@ -186,12 +181,7 @@ def parse_format_value( The specifiers may be nested (two levels maximum), in this case they are ordered as '{0:{1}}, {2:{3}{4}}'. Return None in case of an error. """ - print("in parse") - print("format_value", format_value) - print("ctx", ctx) - print("msg", msg) top_targets = find_non_escaped_targets(format_value, ctx, msg) - print("top_targets", top_targets) if top_targets is None: return None @@ -333,12 +323,7 @@ def check_str_format_call(self, call: CallExpr, format_value: str) -> None: - 's' must not accept bytes - non-empty flags are only allowed for numeric types """ - print("in check str formal") - print("Call: ", call) - print("format_value: ", format_value) conv_specs = parse_format_value(format_value, call, self.msg) - # print("conv_specs", conv_specs) - print() if conv_specs is None: return if not self.auto_generate_keys(conv_specs, call): @@ -352,7 +337,6 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ - print("in check specs") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) @@ -414,9 +398,15 @@ def check_specs_in_format_call( get_proper_types(a_type.items) if isinstance(a_type, UnionType) else [a_type] ) for a_type in actual_items: - print("atype", a_type, type(a_type)) if isinstance(a_type, NoneType): - print("isNone") + # Perform type check of alignment specifiers on None + if spec.format_spec and spec.format_spec[1] in {"<", ">", "^"}: + self.msg.fail( + f"Alignment format specifier '{spec.format_spec[1]}' is not supported for NoneType", + call, + code=codes.STRING_FORMATTING, + ) + continue if custom_special_method(a_type, "__format__"): continue self.check_placeholder_type(a_type, expected_type, call) From 49d8fbf4d715c998ead8cc0250fbf5ba7d81ac06 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:21:44 -0400 Subject: [PATCH 03/13] Fix alignment specifier check to be more dynamic --- mypy/checkstrformat.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index d8af510e59ef..320b47c65dab 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -399,14 +399,23 @@ def check_specs_in_format_call( ) for a_type in actual_items: if isinstance(a_type, NoneType): - # Perform type check of alignment specifiers on None - if spec.format_spec and spec.format_spec[1] in {"<", ">", "^"}: - self.msg.fail( - f"Alignment format specifier '{spec.format_spec[1]}' is not supported for NoneType", - call, - code=codes.STRING_FORMATTING, - ) - continue + # Perform type check of alignment specifiers on None + if spec.format_spec and any(c in spec.format_spec for c in "<>^"): + specifierIndex = -1 + for i in range(len("<>^")): + if spec.format_spec[i] in "<>^": + specifierIndex = i + if specifierIndex > -1: + self.msg.fail( + ( + f'Alignment format specifier ' + f'"{spec.format_spec[specifierIndex]}" ' + f'is not supported for None' + ), + call, + code=codes.STRING_FORMATTING, + ) + continue if custom_special_method(a_type, "__format__"): continue self.check_placeholder_type(a_type, expected_type, call) From c5d0b71829cc4cb324fe1abbfec74a8d19fe7aa3 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:52:36 -0400 Subject: [PATCH 04/13] Move alignment check to special check function --- mypy/checkstrformat.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 320b47c65dab..b6194b2f0faf 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -337,6 +337,7 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ + print("in format call") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) @@ -398,24 +399,6 @@ def check_specs_in_format_call( get_proper_types(a_type.items) if isinstance(a_type, UnionType) else [a_type] ) for a_type in actual_items: - if isinstance(a_type, NoneType): - # Perform type check of alignment specifiers on None - if spec.format_spec and any(c in spec.format_spec for c in "<>^"): - specifierIndex = -1 - for i in range(len("<>^")): - if spec.format_spec[i] in "<>^": - specifierIndex = i - if specifierIndex > -1: - self.msg.fail( - ( - f'Alignment format specifier ' - f'"{spec.format_spec[specifierIndex]}" ' - f'is not supported for None' - ), - call, - code=codes.STRING_FORMATTING, - ) - continue if custom_special_method(a_type, "__format__"): continue self.check_placeholder_type(a_type, expected_type, call) @@ -466,6 +449,23 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) + if isinstance(actual_type, NoneType): + # Perform type check of alignment specifiers on None + if spec.format_spec and any(c in spec.format_spec for c in "<>^"): + specifierIndex = -1 + for i in range(len("<>^")): + if spec.format_spec[i] in "<>^": + specifierIndex = i + if specifierIndex > -1: + self.msg.fail( + ( + f'Alignment format specifier ' + f'"{spec.format_spec[specifierIndex]}" ' + f'is not supported for None' + ), + call, + code=codes.STRING_FORMATTING, + ) def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. From 31542a16412ffbc5fdc86f9c6a6439b1e01d81a6 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Apr 2025 15:33:24 -0400 Subject: [PATCH 05/13] Added unit tests to check for Error Message for None argument and also test for valid inputs --- test-data/unit/check-formatting.test | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test-data/unit/check-formatting.test b/test-data/unit/check-formatting.test index dce26b37dfc8..ae46bdf53f60 100644 --- a/test-data/unit/check-formatting.test +++ b/test-data/unit/check-formatting.test @@ -265,6 +265,16 @@ b'%c' % (123) -- ------------------ +[case testFormatCallNoneAlignment] +'{:<1}'.format(None) # E: Alignment format specifier "<" is not supported for None +'{:>1}'.format(None) # E: Alignment format specifier ">" is not supported for None +'{:^1}'.format(None) # E: Alignment format specifier "^" is not supported for None + +'{:<10}'.format('16') # OK +'{:>10}'.format('16') # OK +'{:^10}'.format('16') # OK +[builtins fixtures/primitives.pyi] + [case testFormatCallParseErrors] '}'.format() # E: Invalid conversion specifier in format string: unexpected } '{'.format() # E: Invalid conversion specifier in format string: unmatched { From feda1d3fb0069c32b2d16e9d11fcd3ca810210ce Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Apr 2025 17:27:35 -0400 Subject: [PATCH 06/13] Attempt to recognize no __format__ functions must result in error --- mypy/checkstrformat.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index b6194b2f0faf..623839fd098f 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -55,6 +55,7 @@ NoneType, TupleType, Type, + TypeType, TypeOfAny, TypeVarTupleType, TypeVarType, @@ -63,6 +64,7 @@ find_unpack_in_list, get_proper_type, get_proper_types, + DeletedType ) FormatStringExpr: _TypeAlias = Union[StrExpr, BytesExpr] @@ -337,7 +339,7 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ - print("in format call") + #print("in format call") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) @@ -449,6 +451,7 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) + actual_type = get_proper_type(actual_type) if isinstance(actual_type, NoneType): # Perform type check of alignment specifiers on None if spec.format_spec and any(c in spec.format_spec for c in "<>^"): @@ -466,6 +469,34 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) + if spec.format_spec and not self.type_supports_formatting(actual_type): + self.msg.fail( + f'"{actual_type}" does not support custom formatting (no __format__ method)', + call, + code=codes.STRING_FORMATTING, + ) + + def type_supports_formatting(self, actual_type: Type) -> bool: + actual_type = get_proper_type(actual_type) + if isinstance(actual_type, (NoneType, LiteralType, AnyType, TupleType, TypeType, DeletedType)): + return True; + if isinstance(actual_type, Instance): + fullname = actual_type.type.fullname + substring = ["builtins.", "mypy.", "mypyc.", "typing.", "types.", "uuid.", "pathlib.", + "_pytest.", "inspect.", "os.", "sys.", "re.", "collections."] + for s in substring: + if (s in fullname): + return True + # if isinstance(actual_type, NoneType) or isinstance(actual_type, LiteralType): + # return True + # if isinstance(actual_type, Instance): + # fullname = actual_type.type.fullname + # substring = ["builtins.", "mypyc.", "mypy.", "uuid.", "uuid.", "pathlib."] + # for s in substring: + # if (s in fullname): + # return True + return False + def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. From 2286f46e4ff4a1bd2234bb568ced8b7d52d9d955 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Apr 2025 17:30:13 -0400 Subject: [PATCH 07/13] Deleted commented out code --- mypy/checkstrformat.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 623839fd098f..d33b78acbd95 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -487,14 +487,6 @@ def type_supports_formatting(self, actual_type: Type) -> bool: for s in substring: if (s in fullname): return True - # if isinstance(actual_type, NoneType) or isinstance(actual_type, LiteralType): - # return True - # if isinstance(actual_type, Instance): - # fullname = actual_type.type.fullname - # substring = ["builtins.", "mypyc.", "mypy.", "uuid.", "uuid.", "pathlib."] - # for s in substring: - # if (s in fullname): - # return True return False From 8b94be8b12ab7b6bc963ef61a80c5ae7b022fce6 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:50:38 -0400 Subject: [PATCH 08/13] Remove print for test --- mypy/checkstrformat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index b6194b2f0faf..12af4e2fbb61 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -337,7 +337,6 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ - print("in format call") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) From d4f70fbddeb1d7e90a38adf70d177afd77bfbb83 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:04:33 -0400 Subject: [PATCH 09/13] Fix isinstance call with get_property_type --- mypy/checkstrformat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 12af4e2fbb61..a9512704d664 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -448,7 +448,8 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) - if isinstance(actual_type, NoneType): + a_type = get_proper_type(actual_type) + if isinstance(a_type, NoneType): # Perform type check of alignment specifiers on None if spec.format_spec and any(c in spec.format_spec for c in "<>^"): specifierIndex = -1 From e3d002e240d881a99a85bf27cd359988f9f369a9 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:38:28 -0400 Subject: [PATCH 10/13] Fix lint for alignment specifier on none --- mypy/checkstrformat.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index a9512704d664..dd38870ca579 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -458,14 +458,14 @@ def perform_special_format_checks( specifierIndex = i if specifierIndex > -1: self.msg.fail( - ( - f'Alignment format specifier ' - f'"{spec.format_spec[specifierIndex]}" ' - f'is not supported for None' - ), - call, - code=codes.STRING_FORMATTING, - ) + ( + f"Alignment format specifier " + f'"{spec.format_spec[specifierIndex]}" ' + f"is not supported for None" + ), + call, + code=codes.STRING_FORMATTING, + ) def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. From 693486e0a8b66e424b675cc73954a62741783718 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Apr 2025 22:02:15 -0400 Subject: [PATCH 11/13] Format changed by Linter --- mypy/checkstrformat.py | 54 ++++++++++++++++++++++++++---------------- test.py | 7 ++++++ 2 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 test.py diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index d33b78acbd95..613fcf149a07 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -50,13 +50,14 @@ from mypy.typeops import custom_special_method from mypy.types import ( AnyType, + DeletedType, Instance, LiteralType, NoneType, TupleType, Type, - TypeType, TypeOfAny, + TypeType, TypeVarTupleType, TypeVarType, UnionType, @@ -64,7 +65,6 @@ find_unpack_in_list, get_proper_type, get_proper_types, - DeletedType ) FormatStringExpr: _TypeAlias = Union[StrExpr, BytesExpr] @@ -339,7 +339,7 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ - #print("in format call") + # print("in format call") assert all(s.key for s in specs), "Keys must be auto-generated first!" replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs]) assert len(replacements) == len(specs) @@ -451,7 +451,7 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) - actual_type = get_proper_type(actual_type) + actual_type = get_proper_type(actual_type) if isinstance(actual_type, NoneType): # Perform type check of alignment specifiers on None if spec.format_spec and any(c in spec.format_spec for c in "<>^"): @@ -461,34 +461,48 @@ def perform_special_format_checks( specifierIndex = i if specifierIndex > -1: self.msg.fail( - ( - f'Alignment format specifier ' - f'"{spec.format_spec[specifierIndex]}" ' - f'is not supported for None' - ), - call, - code=codes.STRING_FORMATTING, - ) + ( + f"Alignment format specifier " + f'"{spec.format_spec[specifierIndex]}" ' + f"is not supported for None" + ), + call, + code=codes.STRING_FORMATTING, + ) if spec.format_spec and not self.type_supports_formatting(actual_type): self.msg.fail( f'"{actual_type}" does not support custom formatting (no __format__ method)', call, code=codes.STRING_FORMATTING, ) - + def type_supports_formatting(self, actual_type: Type) -> bool: - actual_type = get_proper_type(actual_type) - if isinstance(actual_type, (NoneType, LiteralType, AnyType, TupleType, TypeType, DeletedType)): - return True; + actual_type = get_proper_type(actual_type) + if isinstance( + actual_type, (NoneType, LiteralType, AnyType, TupleType, TypeType, DeletedType) + ): + return True if isinstance(actual_type, Instance): fullname = actual_type.type.fullname - substring = ["builtins.", "mypy.", "mypyc.", "typing.", "types.", "uuid.", "pathlib.", - "_pytest.", "inspect.", "os.", "sys.", "re.", "collections."] + substring = [ + "builtins.", + "mypy.", + "mypyc.", + "typing.", + "types.", + "uuid.", + "pathlib.", + "_pytest.", + "inspect.", + "os.", + "sys.", + "re.", + "collections.", + ] for s in substring: - if (s in fullname): + if s in fullname: return True return False - def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. diff --git a/test.py b/test.py new file mode 100644 index 000000000000..b0a687519dfb --- /dev/null +++ b/test.py @@ -0,0 +1,7 @@ +class GoodFormat: + def __format__(self, format_spec): + return f"" +"{:*^15}".format(GoodFormat()) +class Foo1: + def __str__(self): return "hello" +"{:*^15}".format(Foo1()) \ No newline at end of file From c979af9ab1bbb47b8de4c27c6cc4438c791d5ae2 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Apr 2025 23:06:52 -0400 Subject: [PATCH 12/13] Extend check for format string specifiers and added more unit tests --- mypy/checkstrformat.py | 38 +++++++++++++++------------- test-data/unit/check-formatting.test | 21 +++++++++++++++ test.py | 7 ----- 3 files changed, 42 insertions(+), 24 deletions(-) delete mode 100644 test.py diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index b6618b562675..176ef8e035c8 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -44,6 +44,7 @@ StrExpr, TempNode, TupleExpr, + Var, ) from mypy.parse import parse from mypy.subtypes import is_subtype @@ -452,24 +453,27 @@ def perform_special_format_checks( code=codes.STRING_FORMATTING, ) - a_type = get_proper_type(actual_type) - if isinstance(a_type, NoneType): + if isinstance(get_proper_type(actual_type), NoneType): # Perform type check of alignment specifiers on None - if spec.format_spec and any(c in spec.format_spec for c in "<>^"): - specifierIndex = -1 - for i in range(len("<>^")): - if spec.format_spec[i] in "<>^": - specifierIndex = i - if specifierIndex > -1: - self.msg.fail( - ( - f"Alignment format specifier " - f'"{spec.format_spec[specifierIndex]}" ' - f"is not supported for None" - ), - call, - code=codes.STRING_FORMATTING, - ) + # If spec.format_spec is None then we use "" instead of avoid crashing + specifier_char = None + if spec.non_standard_format_spec == True and isinstance(call.args[-1], StrExpr): + arg = call.args[-1].value + specifier_char = next((c for c in (arg or "") if c in "<>^"), None) + elif isinstance(spec.format_spec, str): + specifier_char = next((c for c in (spec.format_spec or "") if c in "<>^"), None) + + if specifier_char: + self.msg.fail( + ( + f"Alignment format specifier " + f'"{specifier_char}" ' + f"is not supported for None" + ), + call, + code=codes.STRING_FORMATTING, + ) + def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. diff --git a/test-data/unit/check-formatting.test b/test-data/unit/check-formatting.test index ae46bdf53f60..9bd77cce4aa3 100644 --- a/test-data/unit/check-formatting.test +++ b/test-data/unit/check-formatting.test @@ -266,6 +266,8 @@ b'%c' % (123) [case testFormatCallNoneAlignment] +from typing import Optional + '{:<1}'.format(None) # E: Alignment format specifier "<" is not supported for None '{:>1}'.format(None) # E: Alignment format specifier ">" is not supported for None '{:^1}'.format(None) # E: Alignment format specifier "^" is not supported for None @@ -273,6 +275,25 @@ b'%c' % (123) '{:<10}'.format('16') # OK '{:>10}'.format('16') # OK '{:^10}'.format('16') # OK + +'{!s:<5}'.format(None) # OK +'{!s:>5}'.format(None) # OK +'{!s:^5}'.format(None) # OK + +f"{None!s:<5}" # OK +f"{None!s:>5}" # OK +f"{None!s:^5}" # OK + + +f"{None:<5}" # E: Alignment format specifier "<" is not supported for None +f"{None:>5}" # E: Alignment format specifier ">" is not supported for None +f"{None:^5}" # E: Alignment format specifier "^" is not supported for None + +my_var: Optional[str] = None +"{:<2}".format(my_var) # E: Alignment format specifier "<" is not supported for None +my_var = "test" +"{:>2}".format(my_var) # OK + [builtins fixtures/primitives.pyi] [case testFormatCallParseErrors] diff --git a/test.py b/test.py deleted file mode 100644 index b0a687519dfb..000000000000 --- a/test.py +++ /dev/null @@ -1,7 +0,0 @@ -class GoodFormat: - def __format__(self, format_spec): - return f"" -"{:*^15}".format(GoodFormat()) -class Foo1: - def __str__(self): return "hello" -"{:*^15}".format(Foo1()) \ No newline at end of file From d46f5621573714cd0e5b34cfe9b51c9db34eabe0 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Apr 2025 23:35:51 -0400 Subject: [PATCH 13/13] Fix lint error --- mypy/checkstrformat.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 176ef8e035c8..0d52a205c286 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -44,21 +44,18 @@ StrExpr, TempNode, TupleExpr, - Var, ) from mypy.parse import parse from mypy.subtypes import is_subtype from mypy.typeops import custom_special_method from mypy.types import ( AnyType, - DeletedType, Instance, LiteralType, NoneType, TupleType, Type, TypeOfAny, - TypeType, TypeVarTupleType, TypeVarType, UnionType, @@ -457,12 +454,12 @@ def perform_special_format_checks( # Perform type check of alignment specifiers on None # If spec.format_spec is None then we use "" instead of avoid crashing specifier_char = None - if spec.non_standard_format_spec == True and isinstance(call.args[-1], StrExpr): + if spec.non_standard_format_spec and isinstance(call.args[-1], StrExpr): arg = call.args[-1].value specifier_char = next((c for c in (arg or "") if c in "<>^"), None) elif isinstance(spec.format_spec, str): specifier_char = next((c for c in (spec.format_spec or "") if c in "<>^"), None) - + if specifier_char: self.msg.fail( ( @@ -473,7 +470,6 @@ def perform_special_format_checks( call, code=codes.STRING_FORMATTING, ) - def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call.