diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 45075bd37552..0d52a205c286 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -52,6 +52,7 @@ AnyType, Instance, LiteralType, + NoneType, TupleType, Type, TypeOfAny, @@ -98,7 +99,6 @@ 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![^:])?" - # Format specification (optional) follows its own mini-language: if not custom_spec: # Fill and align is valid for all builtin types. @@ -113,7 +113,6 @@ def compile_new_format_re(custom_spec: bool) -> Pattern[str]: else: # Custom types can define their own form_spec using __format__(). format_spec = r"(?P:.*)?" - return re.compile(field + conversion + format_spec) @@ -338,6 +337,7 @@ def check_specs_in_format_call( The core logic for format checking is implemented in this method. """ + 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) @@ -450,6 +450,27 @@ def perform_special_format_checks( code=codes.STRING_FORMATTING, ) + if isinstance(get_proper_type(actual_type), NoneType): + # 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 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 dce26b37dfc8..9bd77cce4aa3 100644 --- a/test-data/unit/check-formatting.test +++ b/test-data/unit/check-formatting.test @@ -265,6 +265,37 @@ 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 + +'{:<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] '}'.format() # E: Invalid conversion specifier in format string: unexpected } '{'.format() # E: Invalid conversion specifier in format string: unmatched {