Skip to content

Migrate checkstrformat to use ErrorMessage class #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 29 additions & 56 deletions mypy/checkstrformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,22 +177,19 @@ def parse_format_value(format_value: str, ctx: Context, msg: MessageBuilder,
custom_match, start_pos=start_pos,
non_standard_format_spec=True)
else:
msg.fail('Invalid conversion specifier in format string',
ctx, code=codes.STRING_FORMATTING)
msg.fail(message_registry.FORMAT_STR_INVALID_SPECIFIER, ctx)
return None

if conv_spec.key and ('{' in conv_spec.key or '}' in conv_spec.key):
msg.fail('Conversion value must not contain { or }',
ctx, code=codes.STRING_FORMATTING)
msg.fail(message_registry.FORMAT_STR_BRACES_IN_SPECIFIER, ctx)
return None
result.append(conv_spec)

# Parse nested conversions that are allowed in format specifier.
if (conv_spec.format_spec and conv_spec.non_standard_format_spec and
('{' in conv_spec.format_spec or '}' in conv_spec.format_spec)):
if nested:
msg.fail('Formatting nesting must be at most two levels deep',
ctx, code=codes.STRING_FORMATTING)
msg.fail(message_registry.FORMAT_STR_NESTING_ATMOST_TWO_LEVELS, ctx)
return None
sub_conv_specs = parse_format_value(conv_spec.format_spec, ctx, msg,
nested=True)
Expand Down Expand Up @@ -230,8 +227,7 @@ def find_non_escaped_targets(format_value: str, ctx: Context,
if pos < len(format_value) - 1 and format_value[pos + 1] == '}':
pos += 1
else:
msg.fail('Invalid conversion specifier in format string:'
' unexpected }', ctx, code=codes.STRING_FORMATTING)
msg.fail(message_registry.FORMAT_STR_UNEXPECTED_RBRACE, ctx)
return None
else:
# Adjust nesting level, then either continue adding chars or move on.
Expand All @@ -246,8 +242,7 @@ def find_non_escaped_targets(format_value: str, ctx: Context,
next_spec = ''
pos += 1
if nesting:
msg.fail('Invalid conversion specifier in format string:'
' unmatched {', ctx, code=codes.STRING_FORMATTING)
msg.fail(message_registry.FORMAT_STR_UNMATCHED_LBRACE, ctx)
return None
return result

Expand Down Expand Up @@ -326,9 +321,8 @@ def check_specs_in_format_call(self, call: CallExpr,
if (not custom_special_method(actual_type, '__format__', check_all=True) or
spec.conversion):
# TODO: add support for some custom specs like datetime?
self.msg.fail('Unrecognized format'
' specification "{}"'.format(spec.format_spec[1:]),
call, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.UNRECOGNIZED_FORMAT_SPEC.format(
spec.format_spec[1:]), call)
continue
# Adjust expected and actual types.
if not spec.conv_type:
Expand All @@ -344,9 +338,8 @@ def check_specs_in_format_call(self, call: CallExpr,
if spec.conversion is not None:
# If the explicit conversion is given, then explicit conversion is called _first_.
if spec.conversion[1] not in 'rsa':
self.msg.fail('Invalid conversion type "{}",'
' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
call, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_CONVERSION_TYPE.format(
spec.conversion[1]), call)
actual_type = self.named_type('builtins.str')

# Perform the checks for given types.
Expand Down Expand Up @@ -379,19 +372,14 @@ def perform_special_format_checks(self, spec: ConversionSpecifier, call: CallExp
if self.chk.options.python_version >= (3, 0):
if (has_type_component(actual_type, 'builtins.bytes') and
not custom_special_method(actual_type, '__str__')):
self.msg.fail(
'On Python 3 formatting "b\'abc\'" with "{}" '
'produces "b\'abc\'", not "abc"; '
'use "{!r}" if this is desired behavior',
call, code=codes.STR_BYTES_PY3)
self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR, call)
if spec.flags:
numeric_types = UnionType([self.named_type('builtins.int'),
self.named_type('builtins.float')])
if (spec.conv_type and spec.conv_type not in NUMERIC_TYPES_NEW or
not spec.conv_type and not is_subtype(actual_type, numeric_types) and
not custom_special_method(actual_type, '__format__')):
self.msg.fail('Numeric flags are only allowed for numeric types', call,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_NUMERIC_FLAG, call)

def find_replacements_in_call(self, call: CallExpr,
keys: List[str]) -> List[Expression]:
Expand All @@ -405,16 +393,14 @@ def find_replacements_in_call(self, call: CallExpr,
if key.isdecimal():
expr = self.get_expr_by_position(int(key), call)
if not expr:
self.msg.fail('Cannot find replacement for positional'
' format specifier {}'.format(key), call,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_REPLACEMENT_NOT_FOUND.format(key),
call)
expr = TempNode(AnyType(TypeOfAny.from_error))
else:
expr = self.get_expr_by_name(key, call)
if not expr:
self.msg.fail('Cannot find replacement for named'
' format specifier "{}"'.format(key), call,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND.format(
key), call)
expr = TempNode(AnyType(TypeOfAny.from_error))
result.append(expr)
if not isinstance(expr, TempNode):
Expand Down Expand Up @@ -483,8 +469,7 @@ def auto_generate_keys(self, all_specs: List[ConversionSpecifier],
some_defined = any(s.key and s.key.isdecimal() for s in all_specs)
all_defined = all(bool(s.key) for s in all_specs)
if some_defined and not all_defined:
self.msg.fail('Cannot combine automatic field numbering and'
' manual field specification', ctx, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_PARTIAL_FIELD_NUMBERING, ctx)
return False
if all_defined:
return True
Expand Down Expand Up @@ -519,8 +504,7 @@ def apply_field_accessors(self, spec: ConversionSpecifier, repl: Expression,
dummy, fnam="<format>", module=None, options=self.chk.options, errors=temp_errors
)
if temp_errors.is_errors():
self.msg.fail('Syntax error in format specifier "{}"'.format(spec.field),
ctx, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_SYNTAX_ERROR.format(spec.field), ctx)
return TempNode(AnyType(TypeOfAny.from_error))

# These asserts are guaranteed by the original regexp.
Expand Down Expand Up @@ -553,9 +537,8 @@ class User(TypedDict):
'{[id]:d} -> {[name]}'.format(u)
"""
if not isinstance(temp_ast, (MemberExpr, IndexExpr)):
self.msg.fail('Only index and member expressions are allowed in'
' format field accessors; got "{}"'.format(spec.field),
ctx, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_ACCESSOR_EXPR.format(spec.field),
ctx)
return False
if isinstance(temp_ast, MemberExpr):
node = temp_ast.expr
Expand All @@ -564,9 +547,8 @@ class User(TypedDict):
if not isinstance(temp_ast.index, (NameExpr, IntExpr)):
assert spec.key, "Call this method only after auto-generating keys!"
assert spec.field
self.msg.fail('Invalid index expression in format field'
' accessor "{}"'.format(spec.field[len(spec.key):]), ctx,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_INDEX_ACCESSOR.format(
spec.field[len(spec.key):]), ctx)
return False
if isinstance(temp_ast.index, NameExpr):
temp_ast.index = StrExpr(temp_ast.index.name)
Expand Down Expand Up @@ -595,8 +577,7 @@ def check_str_interpolation(self,
specifiers = parse_conversion_specifiers(expr.value)
has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr)
if isinstance(expr, BytesExpr) and (3, 0) <= self.chk.options.python_version < (3, 5):
self.msg.fail('Bytes formatting is only supported in Python 3.5 and later',
replacements, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_BYTES_ABOVE_PY35, replacements)
return AnyType(TypeOfAny.from_error)

self.unicode_upcast = False
Expand Down Expand Up @@ -697,8 +678,8 @@ def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
if self.chk.options.python_version >= (3, 0) and isinstance(expr, BytesExpr):
# Special case: for bytes formatting keys must be bytes.
if not isinstance(k, BytesExpr):
self.msg.fail('Dictionary keys in bytes formatting must be bytes,'
' not strings', expr, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES,
expr)
key_str = cast(FormatStringExpr, k).value
mapping[key_str] = self.accept(v)

Expand Down Expand Up @@ -841,11 +822,7 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
# Couple special cases for string formatting.
if self.chk.options.python_version >= (3, 0):
if has_type_component(typ, 'builtins.bytes'):
self.msg.fail(
'On Python 3 formatting "b\'abc\'" with "%s" '
'produces "b\'abc\'", not "abc"; '
'use "%r" if this is desired behavior',
context, code=codes.STR_BYTES_PY3)
self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR_OLD, context)
return False
if self.chk.options.python_version < (3, 0):
if has_type_component(typ, 'builtins.unicode'):
Expand All @@ -854,8 +831,7 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
# A special case for bytes formatting: b'%s' actually requires bytes on Python 3.
if self.chk.options.python_version >= (3, 0):
if has_type_component(typ, 'builtins.str'):
self.msg.fail("On Python 3 b'%s' requires bytes, not string", context,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_BYTES_REQUIRED_PY3, context)
return False
return True

Expand Down Expand Up @@ -910,18 +886,15 @@ def conversion_type(self, p: str, context: Context, expr: FormatStringExpr,
INT_TYPES = REQUIRE_INT_NEW if format_call else REQUIRE_INT_OLD
if p == 'b' and not format_call:
if self.chk.options.python_version < (3, 5):
self.msg.fail('Format character "b" is only supported in Python 3.5 and later',
context, code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35, context)
return None
if not isinstance(expr, BytesExpr):
self.msg.fail('Format character "b" is only supported on bytes patterns', context,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER, context)
return None
return self.named_type('builtins.bytes')
elif p == 'a':
if self.chk.options.python_version < (3, 0):
self.msg.fail('Format character "a" is only supported in Python 3', context,
code=codes.STRING_FORMATTING)
self.msg.fail(message_registry.FORMAT_STR_ASCII_SPECIFIER_PY3, context)
return None
# TODO: return type object?
return AnyType(TypeOfAny.special_form)
Expand Down
4 changes: 2 additions & 2 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ def __str__(self) -> str:
VALID_NEWTYPE: Final[ErrorCode] = ErrorCode(
"valid-newtype", "Check that argument 2 to NewType is valid", "General"
)
STRING_FORMATTING: Final = ErrorCode(
STRING_FORMATTING: Final[ErrorCode] = ErrorCode(
"str-format", "Check that string formatting/interpolation is type-safe", "General"
)
STR_BYTES_PY3: Final = ErrorCode(
STR_BYTES_PY3: Final[ErrorCode] = ErrorCode(
"str-bytes-safe", "Warn about dangerous coercions related to bytes and string types", "General"
)
EXIT_RETURN: Final = ErrorCode(
Expand Down
76 changes: 75 additions & 1 deletion mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,80 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
KWARGS_MUST_BE_LAST: Final = ErrorMessage("A **kwargs argument must be the last argument")
MULTIPLE_KWARGS: Final = ErrorMessage("You may only have one **kwargs argument")

# String formatting checks
FORMAT_STR_INVALID_SPECIFIER: Final = ErrorMessage(
"Invalid conversion specifier in format string", codes.STRING_FORMATTING
)
FORMAT_STR_BRACES_IN_SPECIFIER: Final = ErrorMessage(
"Conversion value must not contain { or }", codes.STRING_FORMATTING
)
FORMAT_STR_NESTING_ATMOST_TWO_LEVELS: Final = ErrorMessage(
"Formatting nesting must be at most two levels deep", codes.STRING_FORMATTING
)
FORMAT_STR_UNEXPECTED_RBRACE: Final = ErrorMessage(
"Invalid conversion specifier in format string: unexpected }", codes.STRING_FORMATTING
)
FORMAT_STR_UNMATCHED_LBRACE: Final = ErrorMessage(
"Invalid conversion specifier in format string: unmatched {", codes.STRING_FORMATTING
)
UNRECOGNIZED_FORMAT_SPEC: Final = ErrorMessage(
'Unrecognized format specification "{}"', codes.STRING_FORMATTING
)
FORMAT_STR_INVALID_CONVERSION_TYPE: Final = ErrorMessage(
'Invalid conversion type "{}", must be one of "r", "s" or "a"', codes.STRING_FORMATTING
)
FORMAT_STR_BYTES_USE_REPR: Final = ErrorMessage(
'On Python 3 formatting "b\'abc\'" with "{}" produces "b\'abc\'", not "abc"; use'
' "{!r}" if this is desired behavior',
codes.STR_BYTES_PY3,
)
FORMAT_STR_BYTES_USE_REPR_OLD: Final = ErrorMessage(
'On Python 3 formatting "b\'abc\'" with "%s" produces "b\'abc\'", not "abc"; use "%r"'
" if this is desired behavior",
codes.STR_BYTES_PY3,
)
FORMAT_STR_INVALID_NUMERIC_FLAG: Final = ErrorMessage(
"Numeric flags are only allowed for numeric types", codes.STRING_FORMATTING
)
FORMAT_STR_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
"Cannot find replacement for positional format specifier {}", codes.STRING_FORMATTING
)
FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
'Cannot find replacement for named format specifier "{}"', codes.STRING_FORMATTING
)
FORMAT_STR_PARTIAL_FIELD_NUMBERING: Final = ErrorMessage(
"Cannot combine automatic field numbering and manual field specification",
codes.STRING_FORMATTING,
)
FORMAT_STR_SYNTAX_ERROR: Final = ErrorMessage(
'Syntax error in format specifier "{}"', codes.STRING_FORMATTING
)
FORMAT_STR_INVALID_ACCESSOR_EXPR: Final = ErrorMessage(
'Only index and member expressions are allowed in format field accessors; got "{}"',
codes.STRING_FORMATTING,
)
FORMAT_STR_INVALID_INDEX_ACCESSOR: Final = ErrorMessage(
'Invalid index expression in format field accessor "{}"', codes.STRING_FORMATTING
)
FORMAT_STR_BYTES_ABOVE_PY35: Final = ErrorMessage(
"Bytes formatting is only supported in Python 3.5 and later", codes.STRING_FORMATTING
)
FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES: Final = ErrorMessage(
"Dictionary keys in bytes formatting must be bytes, not strings", codes.STRING_FORMATTING
)
FORMAT_STR_BYTES_REQUIRED_PY3: Final = ErrorMessage(
"On Python 3 b'%s' requires bytes, not string", codes.STRING_FORMATTING
)
FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35: Final = ErrorMessage(
'Format character "b" is only supported in Python 3.5 and later', codes.STRING_FORMATTING
)
FORMAT_STR_INVALID_BYTES_SPECIFIER: Final = ErrorMessage(
'Format character "b" is only supported on bytes patterns', codes.STRING_FORMATTING
)
FORMAT_STR_ASCII_SPECIFIER_PY3: Final = ErrorMessage(
'Format character "a" is only supported in Python 3', codes.STRING_FORMATTING
)

# Semantic Analysis
METHOD_ATLEAST_ONE_ARG: Final = ErrorMessage('Method must have at least one argument')
OVERLOAD_IMPLEMENTATION_IN_STUB: Final = ErrorMessage(
Expand Down Expand Up @@ -281,7 +355,7 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
)
NO_IMPLICIT_REEXPORT: Final = ErrorMessage(
'Module "{}" does not explicitly export attribute "{}"; implicit reexport disabled',
codes.ATTR_DEFINED
codes.ATTR_DEFINED,
)
INCORRECT_RELATIVE_IMPORT: Final = ErrorMessage("Relative import climbs too many namespaces")
INVALID_TYPE_ALIAS_TARGET: Final = ErrorMessage(
Expand Down
9 changes: 8 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from mypy.sametypes import is_same_type
from mypy.util import unmangle
from mypy.errorcodes import ErrorCode
from mypy.message_registry import ErrorMessage
from mypy import message_registry, errorcodes as codes

TYPES_FOR_UNIMPORTED_HINTS: Final = {
Expand Down Expand Up @@ -189,14 +190,20 @@ def report(self,
end_line=end_line, code=code, allow_dups=allow_dups)

def fail(self,
msg: str,
msg: Union[str, ErrorMessage],
context: Optional[Context],
*,
code: Optional[ErrorCode] = None,
file: Optional[str] = None,
origin: Optional[Context] = None,
allow_dups: bool = False) -> None:
"""Report an error message (unless disabled)."""
# TODO(tushar): Remove `str` support after full migration
if isinstance(msg, ErrorMessage):
self.report(msg.value, context, 'error', code=msg.code, file=file,
origin=origin, allow_dups=allow_dups)
return

self.report(msg, context, 'error', code=code, file=file,
origin=origin, allow_dups=allow_dups)

Expand Down