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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 0d329ddd3c5c11596ecfd37e8f6a615bc353bc8c Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:47:21 -0400 Subject: [PATCH 11/19] Initial approach for distribute Tuple issue --- mypy/subtypes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 71b8b0ba59f5..095243be0ca6 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2,6 +2,7 @@ from collections.abc import Iterable, Iterator from contextlib import contextmanager +from itertools import product from typing import Any, Callable, Final, TypeVar, cast from typing_extensions import TypeAlias as _TypeAlias @@ -34,6 +35,7 @@ ) from mypy.options import Options from mypy.state import state +from mypy.typeops import make_simplified_union from mypy.types import ( MYPYC_NATIVE_INT_NAMES, TUPLE_LIKE_INSTANCE_NAMES, @@ -185,6 +187,27 @@ def is_subtype( # steps we come back to initial call is_subtype(A, B) and immediately return True. with pop_on_exit(type_state.get_assumptions(is_proper=False), left, right): return _is_subtype(left, right, subtype_context, proper_subtype=False) + left = get_proper_type(left) + right = get_proper_type(right) + + # Special case: distribute Tuple unions before fallback subtype check + if isinstance(left, TupleType) and isinstance(right, UnionType): + items = [get_proper_type(item) for item in left.items] + if any(isinstance(item, UnionType) for item in items): + expanded = [] + for item in items: + if isinstance(item, UnionType): + expanded.append(item.items) + else: + expanded.append([item]) + distributed = [] + for combo in product(*expanded): + fb = left.partial_fallback + if hasattr(left, "fallback") and left.fallback is not None: + fb = left.fallback + distributed.append(TupleType(list(combo), fallback=fb)) + simplified = make_simplified_union(distributed) + return _is_subtype(simplified, right, subtype_context, proper_subtype=False) return _is_subtype(left, right, subtype_context, proper_subtype=False) From 693486e0a8b66e424b675cc73954a62741783718 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Apr 2025 22:02:15 -0400 Subject: [PATCH 12/19] 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 bcf28be34e4eee92242b043b198057a2cc85d70f Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:26:25 -0400 Subject: [PATCH 13/19] Attempt to fix max recursive depth for distr tuple --- mypy/subtypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 095243be0ca6..0ec7071bcf44 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -207,6 +207,8 @@ def is_subtype( fb = left.fallback distributed.append(TupleType(list(combo), fallback=fb)) simplified = make_simplified_union(distributed) + if is_equivalent(simplified, right): + return True return _is_subtype(simplified, right, subtype_context, proper_subtype=False) return _is_subtype(left, right, subtype_context, proper_subtype=False) From 066a0f3946aa537b28795e4df1a68c8fc1bb11cc Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:58:09 -0400 Subject: [PATCH 14/19] Move code of distribute tuple --- mypy/subtypes.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0ec7071bcf44..0f459d3c0cec 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -189,27 +189,6 @@ def is_subtype( return _is_subtype(left, right, subtype_context, proper_subtype=False) left = get_proper_type(left) right = get_proper_type(right) - - # Special case: distribute Tuple unions before fallback subtype check - if isinstance(left, TupleType) and isinstance(right, UnionType): - items = [get_proper_type(item) for item in left.items] - if any(isinstance(item, UnionType) for item in items): - expanded = [] - for item in items: - if isinstance(item, UnionType): - expanded.append(item.items) - else: - expanded.append([item]) - distributed = [] - for combo in product(*expanded): - fb = left.partial_fallback - if hasattr(left, "fallback") and left.fallback is not None: - fb = left.fallback - distributed.append(TupleType(list(combo), fallback=fb)) - simplified = make_simplified_union(distributed) - if is_equivalent(simplified, right): - return True - return _is_subtype(simplified, right, subtype_context, proper_subtype=False) return _is_subtype(left, right, subtype_context, proper_subtype=False) @@ -328,7 +307,23 @@ def _is_subtype( # TODO: should we consider all types proper subtypes of UnboundType and/or # ErasedType as we do for non-proper subtyping. return True - + if isinstance(left, TupleType) and isinstance(right, UnionType): + items = [get_proper_type(item) for item in left.items] + if any(isinstance(item, UnionType) for item in items): + expanded = [] + for item in items: + if isinstance(item, UnionType): + expanded.append(item.items) + else: + expanded.append([item]) + distributed = [] + for combo in product(*expanded): + fb = left.partial_fallback + if hasattr(left, "fallback") and left.fallback is not None: + fb = left.fallback + distributed.append(TupleType(list(combo), fallback=fb)) + simplified = make_simplified_union(distributed) + return _is_subtype(simplified, right, subtype_context, proper_subtype=False) if isinstance(right, UnionType) and not isinstance(left, UnionType): # Normally, when 'left' is not itself a union, the only way # 'left' can be a subtype of the union 'right' is if it is a From c979af9ab1bbb47b8de4c27c6cc4438c791d5ae2 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Apr 2025 23:06:52 -0400 Subject: [PATCH 15/19] 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 16/19] 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. From ade0d1b9cadc84664bedd7abe9732c43df968c33 Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Fri, 25 Apr 2025 23:48:38 -0400 Subject: [PATCH 17/19] Clean up and add comments --- mypy/subtypes.py | 56 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0f459d3c0cec..420407620e5c 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2,7 +2,6 @@ from collections.abc import Iterable, Iterator from contextlib import contextmanager -from itertools import product from typing import Any, Callable, Final, TypeVar, cast from typing_extensions import TypeAlias as _TypeAlias @@ -35,7 +34,6 @@ ) from mypy.options import Options from mypy.state import state -from mypy.typeops import make_simplified_union from mypy.types import ( MYPYC_NATIVE_INT_NAMES, TUPLE_LIKE_INSTANCE_NAMES, @@ -286,6 +284,21 @@ def is_same_type( ) +# This is a helper function used to check for recursive type of distributed tuple +def structurally_recursive(typ: Type, seen: set[Type] | None = None) -> bool: + if seen is None: + seen = set() + typ = get_proper_type(typ) + if typ in seen: + return True + seen.add(typ) + if isinstance(typ, UnionType): + return any(structurally_recursive(item, seen.copy()) for item in typ.items) + if isinstance(typ, TupleType): + return any(structurally_recursive(item, seen.copy()) for item in typ.items) + return False + + # This is a common entry point for subtyping checks (both proper and non-proper). # Never call this private function directly, use the public versions. def _is_subtype( @@ -308,22 +321,29 @@ def _is_subtype( # ErasedType as we do for non-proper subtyping. return True if isinstance(left, TupleType) and isinstance(right, UnionType): - items = [get_proper_type(item) for item in left.items] - if any(isinstance(item, UnionType) for item in items): - expanded = [] - for item in items: - if isinstance(item, UnionType): - expanded.append(item.items) - else: - expanded.append([item]) - distributed = [] - for combo in product(*expanded): - fb = left.partial_fallback - if hasattr(left, "fallback") and left.fallback is not None: - fb = left.fallback - distributed.append(TupleType(list(combo), fallback=fb)) - simplified = make_simplified_union(distributed) - return _is_subtype(simplified, right, subtype_context, proper_subtype=False) + # check only if not recursive type because if recursive type, + # test run into maximum recursive depth reached + if not structurally_recursive(left) and not structurally_recursive(right): + fallback = left.partial_fallback + tuple_items = left.items + if hasattr(left, "fallback") and left.fallback is not None: + fallback = left.fallback + for i in range(len(tuple_items)): + uitems = tuple_items[i] + uitems_type = get_proper_type(uitems) + if isinstance(uitems_type, UnionType): + new_tuples = [ + TupleType( + tuple_items[:i] + [uitem] + tuple_items[i + 1 :], fallback=fallback + ) + for uitem in uitems_type.items + ] + result = [ + _is_subtype(t, right, subtype_context, proper_subtype=False) + for t in new_tuples + ] + inverted_list = [not item for item in result] + return not any(inverted_list) if isinstance(right, UnionType) and not isinstance(left, UnionType): # Normally, when 'left' is not itself a union, the only way # 'left' can be a subtype of the union 'right' is if it is a From 6949731b665ba800bd53e06b4e1e09fd5d08f95d Mon Sep 17 00:00:00 2001 From: VyZhu <119977752+VyZhu@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:11:02 -0400 Subject: [PATCH 18/19] Change function name to match pattern --- mypy/subtypes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 420407620e5c..850853fc23ae 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -285,7 +285,7 @@ def is_same_type( # This is a helper function used to check for recursive type of distributed tuple -def structurally_recursive(typ: Type, seen: set[Type] | None = None) -> bool: +def is_structurally_recursive(typ: Type, seen: set[Type] | None = None) -> bool: if seen is None: seen = set() typ = get_proper_type(typ) @@ -293,9 +293,9 @@ def structurally_recursive(typ: Type, seen: set[Type] | None = None) -> bool: return True seen.add(typ) if isinstance(typ, UnionType): - return any(structurally_recursive(item, seen.copy()) for item in typ.items) + return any(is_structurally_recursive(item, seen.copy()) for item in typ.items) if isinstance(typ, TupleType): - return any(structurally_recursive(item, seen.copy()) for item in typ.items) + return any(is_structurally_recursive(item, seen.copy()) for item in typ.items) return False @@ -323,7 +323,7 @@ def _is_subtype( if isinstance(left, TupleType) and isinstance(right, UnionType): # check only if not recursive type because if recursive type, # test run into maximum recursive depth reached - if not structurally_recursive(left) and not structurally_recursive(right): + if not is_structurally_recursive(left) and not is_structurally_recursive(right): fallback = left.partial_fallback tuple_items = left.items if hasattr(left, "fallback") and left.fallback is not None: From e5e24305aa91ba3c619d8f8cd289bce3f9c0629e Mon Sep 17 00:00:00 2001 From: VallinZ <107499985+VallinZ@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:20:16 -0400 Subject: [PATCH 19/19] Revert "Attempt to fix distribute Tuple issue " --- mypy/subtypes.py | 42 +----------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 850853fc23ae..71b8b0ba59f5 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -185,8 +185,6 @@ def is_subtype( # steps we come back to initial call is_subtype(A, B) and immediately return True. with pop_on_exit(type_state.get_assumptions(is_proper=False), left, right): return _is_subtype(left, right, subtype_context, proper_subtype=False) - left = get_proper_type(left) - right = get_proper_type(right) return _is_subtype(left, right, subtype_context, proper_subtype=False) @@ -284,21 +282,6 @@ def is_same_type( ) -# This is a helper function used to check for recursive type of distributed tuple -def is_structurally_recursive(typ: Type, seen: set[Type] | None = None) -> bool: - if seen is None: - seen = set() - typ = get_proper_type(typ) - if typ in seen: - return True - seen.add(typ) - if isinstance(typ, UnionType): - return any(is_structurally_recursive(item, seen.copy()) for item in typ.items) - if isinstance(typ, TupleType): - return any(is_structurally_recursive(item, seen.copy()) for item in typ.items) - return False - - # This is a common entry point for subtyping checks (both proper and non-proper). # Never call this private function directly, use the public versions. def _is_subtype( @@ -320,30 +303,7 @@ def _is_subtype( # TODO: should we consider all types proper subtypes of UnboundType and/or # ErasedType as we do for non-proper subtyping. return True - if isinstance(left, TupleType) and isinstance(right, UnionType): - # check only if not recursive type because if recursive type, - # test run into maximum recursive depth reached - if not is_structurally_recursive(left) and not is_structurally_recursive(right): - fallback = left.partial_fallback - tuple_items = left.items - if hasattr(left, "fallback") and left.fallback is not None: - fallback = left.fallback - for i in range(len(tuple_items)): - uitems = tuple_items[i] - uitems_type = get_proper_type(uitems) - if isinstance(uitems_type, UnionType): - new_tuples = [ - TupleType( - tuple_items[:i] + [uitem] + tuple_items[i + 1 :], fallback=fallback - ) - for uitem in uitems_type.items - ] - result = [ - _is_subtype(t, right, subtype_context, proper_subtype=False) - for t in new_tuples - ] - inverted_list = [not item for item in result] - return not any(inverted_list) + if isinstance(right, UnionType) and not isinstance(left, UnionType): # Normally, when 'left' is not itself a union, the only way # 'left' can be a subtype of the union 'right' is if it is a