From 393820c86f879ef68552e407fa9869d7444d9835 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Sun, 24 Oct 2021 19:54:42 +0530 Subject: [PATCH 01/37] Add -O/--output CLI option --- mypy/main.py | 3 +++ mypy/options.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/mypy/main.py b/mypy/main.py index 9ecd345126f4..de5bdfd40c9d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -489,6 +489,9 @@ def add_invertible_flag(flag: str, version='%(prog)s ' + __version__, help="Show program's version number and exit", stdout=stdout) + + general_group.add_argument( + '-O', '--output', metavar='FORMAT', help="Set a custom output format") config_group = parser.add_argument_group( title='Config file', diff --git a/mypy/options.py b/mypy/options.py index 3a56add0d0ad..bad3444df5d4 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -302,6 +302,9 @@ def __init__(self) -> None: # -1 means unlimited. self.many_errors_threshold = defaults.MANY_ERRORS_THRESHOLD + # Sets output format + self.output = "" + # To avoid breaking plugin compatibility, keep providing new_semantic_analyzer @property def new_semantic_analyzer(self) -> bool: From 282bd281089af718d48f00dfc2683e60e17c6a1c Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Tue, 26 Oct 2021 01:05:47 +0530 Subject: [PATCH 02/37] Initial formatter setup --- mypy/build.py | 8 +++++++- mypy/error_formatter.py | 25 +++++++++++++++++++++++++ mypy/errors.py | 13 +++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 mypy/error_formatter.py diff --git a/mypy/build.py b/mypy/build.py index ba671d6ea700..fa590365efd7 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -33,6 +33,7 @@ from mypy.checker import TypeChecker from mypy.indirection import TypeIndirectionVisitor from mypy.errors import Errors, CompileError, ErrorInfo, report_internal_error +from mypy.error_formatter import ErrorFormatter, JSONFormatter from mypy.util import ( DecodeError, decode_python_encoding, is_sub_path, get_mypy_comments, module_prefix, read_py_file, hash_digest, is_typeshed_file, is_stub_package_file, get_top_two_prefixes @@ -243,6 +244,7 @@ def _build(sources: List[BuildSource], plugin=plugin, plugins_snapshot=snapshot, errors=errors, + error_formatter=JSONFormatter() if options.output == 'json' else None, flush_errors=flush_errors, fscache=fscache, stdout=stdout, @@ -577,6 +579,7 @@ def __init__(self, data_dir: str, plugin: Plugin, plugins_snapshot: Dict[str, str], errors: Errors, + error_formatter: 'Optional[ErrorFormatter]', flush_errors: Callable[[List[str], bool], None], fscache: FileSystemCache, stdout: TextIO, @@ -589,6 +592,7 @@ def __init__(self, data_dir: str, self.data_dir = data_dir self.errors = errors self.errors.set_ignore_prefix(ignore_prefix) + self.error_formatter = error_formatter self.search_paths = search_paths self.source_set = source_set self.reports = reports @@ -3154,7 +3158,9 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No for id in stale: graph[id].transitive_error = True for id in stale: - manager.flush_errors(manager.errors.file_messages(graph[id].xpath), False) + errors = manager.errors.file_messages( + graph[id].xpath, formatter=manager.error_formatter) + manager.flush_errors(errors, False) graph[id].write_cache() graph[id].mark_as_rechecked() diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py new file mode 100644 index 000000000000..08e798446408 --- /dev/null +++ b/mypy/error_formatter.py @@ -0,0 +1,25 @@ +import json +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mypy.errors import ErrorTuple + + +class ErrorFormatter(ABC): + """Defines how errors are formatted before being printed.""" + @abstractmethod + def report_error(self, error: 'ErrorTuple') -> str: + raise NotImplementedError + +class JSONFormatter(ErrorFormatter): + def report_error(self, error: 'ErrorTuple') -> str: + file, line, column, severity, message, _, errorcode = error + return json.dumps({ + 'file': file, + 'line': line, + 'column': column, + 'severity': severity, + 'message': message, + 'code': None if errorcode is None else errorcode.code, + }) diff --git a/mypy/errors.py b/mypy/errors.py index 3a0e0e14d8b3..7c8fea0e39c2 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -6,6 +6,7 @@ from typing import Tuple, List, TypeVar, Set, Dict, Optional, TextIO, Callable from typing_extensions import Final +from mypy.error_formatter import ErrorFormatter from mypy.scope import Scope from mypy.options import Options @@ -575,19 +576,27 @@ def format_messages(self, error_info: List[ErrorInfo], a.append(' ' * (DEFAULT_SOURCE_OFFSET + column) + '^') return a - def file_messages(self, path: str) -> List[str]: + def file_messages(self, path: str, formatter: ErrorFormatter = None) -> List[str]: """Return a string list of new error messages from a given file. Use a form suitable for displaying to the user. """ if path not in self.error_info_map: return [] + + error_info = self.error_info_map[path] + if formatter is not None: + error_info = [info for info in error_info if not info.hidden] + errors = self.render_messages(self.sort_messages(error_info)) + errors = self.remove_duplicates(errors) + return [formatter.report_error(err) for err in errors] + self.flushed_files.add(path) source_lines = None if self.pretty: assert self.read_source source_lines = self.read_source(path) - return self.format_messages(self.error_info_map[path], source_lines) + return self.format_messages(error_info, source_lines) def new_messages(self) -> List[str]: """Return a string list of new error messages. From ccda5b0e7a71958d11605f59a92575b6847e3ef7 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 27 Oct 2021 23:26:30 +0530 Subject: [PATCH 03/37] Make error_formatter an optional argument --- mypy/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index fa590365efd7..d53a64b4b503 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -579,11 +579,11 @@ def __init__(self, data_dir: str, plugin: Plugin, plugins_snapshot: Dict[str, str], errors: Errors, - error_formatter: 'Optional[ErrorFormatter]', flush_errors: Callable[[List[str], bool], None], fscache: FileSystemCache, stdout: TextIO, stderr: TextIO, + error_formatter: Optional['ErrorFormatter'] = None, ) -> None: self.stats: Dict[str, Any] = {} # Values are ints or floats self.stdout = stdout From c849a77ad8613e9d5cd074c6de0b02cebc73a2b1 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 27 Oct 2021 23:35:06 +0530 Subject: [PATCH 04/37] Fix type annotation --- mypy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 7c8fea0e39c2..8394f498b49d 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -576,7 +576,7 @@ def format_messages(self, error_info: List[ErrorInfo], a.append(' ' * (DEFAULT_SOURCE_OFFSET + column) + '^') return a - def file_messages(self, path: str, formatter: ErrorFormatter = None) -> List[str]: + def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) -> List[str]: """Return a string list of new error messages from a given file. Use a form suitable for displaying to the user. From fd2feabee3f9acd60063a13ff1a9359e3478e4b5 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 27 Oct 2021 23:38:21 +0530 Subject: [PATCH 05/37] Fix whitespace --- mypy/error_formatter.py | 1 + mypy/errors.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 08e798446408..4e04b990cdbc 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -12,6 +12,7 @@ class ErrorFormatter(ABC): def report_error(self, error: 'ErrorTuple') -> str: raise NotImplementedError + class JSONFormatter(ErrorFormatter): def report_error(self, error: 'ErrorTuple') -> str: file, line, column, severity, message, _, errorcode = error diff --git a/mypy/errors.py b/mypy/errors.py index 8394f498b49d..41d305b4a0ad 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -583,7 +583,7 @@ def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) - """ if path not in self.error_info_map: return [] - + error_info = self.error_info_map[path] if formatter is not None: error_info = [info for info in error_info if not info.hidden] From b1880015d94444e24e25515ccdab498ce2257c25 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 27 Oct 2021 23:55:29 +0530 Subject: [PATCH 06/37] Remove whitespace --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index de5bdfd40c9d..c294f28163a1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -489,7 +489,7 @@ def add_invertible_flag(flag: str, version='%(prog)s ' + __version__, help="Show program's version number and exit", stdout=stdout) - + general_group.add_argument( '-O', '--output', metavar='FORMAT', help="Set a custom output format") From bc5ceacbaa232c312086a6befc8f6e8d1cb1da50 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 19 Jan 2022 14:46:41 +0530 Subject: [PATCH 07/37] Add hint property to errors --- mypy/error_formatter.py | 19 +++++++-------- mypy/errors.py | 54 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 4e04b990cdbc..1cea67f7df31 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -3,24 +3,23 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from mypy.errors import ErrorTuple + from mypy.errors import MypyError class ErrorFormatter(ABC): """Defines how errors are formatted before being printed.""" @abstractmethod - def report_error(self, error: 'ErrorTuple') -> str: + def report_error(self, error: 'MypyError') -> str: raise NotImplementedError class JSONFormatter(ErrorFormatter): - def report_error(self, error: 'ErrorTuple') -> str: - file, line, column, severity, message, _, errorcode = error + def report_error(self, error: 'MypyError') -> str: return json.dumps({ - 'file': file, - 'line': line, - 'column': column, - 'severity': severity, - 'message': message, - 'code': None if errorcode is None else errorcode.code, + 'file': error.file_path, + 'line': error.line, + 'column': error.column, + 'message': error.message, + 'hint': error.hint, + 'code': None if error.errorcode is None else error.errorcode.code, }) diff --git a/mypy/errors.py b/mypy/errors.py index ec488fd77982..7b49adb1d7ff 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -588,8 +588,10 @@ def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) - error_info = self.error_info_map[path] if formatter is not None: error_info = [info for info in error_info if not info.hidden] - errors = self.render_messages(self.sort_messages(error_info)) - errors = self.remove_duplicates(errors) + error_tuples = self.render_messages(self.sort_messages(error_info)) + error_tuples = self.remove_duplicates(error_tuples) + + errors = create_errors(error_tuples) return [formatter.report_error(err) for err in errors] self.flushed_files.add(path) @@ -868,3 +870,51 @@ def report_internal_error(err: Exception, # Exit. The caller has nothing more to say. # We use exit code 2 to signal that this is no ordinary error. raise SystemExit(2) + +class MypyError: + def __init__(self, + file_path: str, + line: int, + column: int, + message: str, + hint: str, + errorcode: Optional[ErrorCode]) -> None: + self.file_path = file_path + self.line = line + self.column = column + self.message = message + self.hint = hint + self.errorcode = errorcode + +# (file_path, line, column) +_ErrorLocation = Tuple[str, int, int] + +def create_errors(error_tuples: List[ErrorTuple]) -> List[MypyError]: + errors: List[MypyError] = [] + latest_error_at_location: Dict[_ErrorLocation, MypyError] = {} + + for error_tuple in error_tuples: + file_path, line, column, severity, message, _, errorcode = error_tuple + if file_path is None: + continue + + assert severity in ('error', 'note') + if severity == 'note': + error_location = (file_path, line, column) + error = latest_error_at_location.get(error_location) + if error is None: + # No error tuple found for this hint. Ignoring it + continue + + if error.hint == '': + error.hint = message + else: + error.hint += '\n' + message + + else: + error = MypyError(file_path, line, column, message, "", errorcode) + errors.append(error) + error_location = (file_path, line, column) + latest_error_at_location[error_location] = error + + return errors \ No newline at end of file From 9d29ab0d28da329d91a5633a11f0df4da5a014f4 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 19 Jan 2022 14:57:37 +0530 Subject: [PATCH 08/37] Fix lint issues --- mypy/errors.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 7b49adb1d7ff..950b2191eec5 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -871,6 +871,7 @@ def report_internal_error(err: Exception, # We use exit code 2 to signal that this is no ordinary error. raise SystemExit(2) + class MypyError: def __init__(self, file_path: str, @@ -886,9 +887,11 @@ def __init__(self, self.hint = hint self.errorcode = errorcode + # (file_path, line, column) _ErrorLocation = Tuple[str, int, int] + def create_errors(error_tuples: List[ErrorTuple]) -> List[MypyError]: errors: List[MypyError] = [] latest_error_at_location: Dict[_ErrorLocation, MypyError] = {} @@ -905,7 +908,7 @@ def create_errors(error_tuples: List[ErrorTuple]) -> List[MypyError]: if error is None: # No error tuple found for this hint. Ignoring it continue - + if error.hint == '': error.hint = message else: @@ -917,4 +920,4 @@ def create_errors(error_tuples: List[ErrorTuple]) -> List[MypyError]: error_location = (file_path, line, column) latest_error_at_location[error_location] = error - return errors \ No newline at end of file + return errors From ba8d17f0a0dcbdaad3023d4c6d23e5d5b8c37d66 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 22 Feb 2023 23:42:19 +0530 Subject: [PATCH 09/37] Fix import and typing issues --- mypy/build.py | 2 +- mypy/errors.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 3e798269f12a..61e7165a2a75 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -46,7 +46,7 @@ import mypy.semanal_main from mypy.checker import TypeChecker -from mypy.errors import Errors, CompileError, ErrorInfo, Errors, report_internal_error +from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error from mypy.error_formatter import ErrorFormatter, JSONFormatter from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder diff --git a/mypy/errors.py b/mypy/errors.py index cf32e8828871..1b8e685f47c8 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1227,9 +1227,9 @@ def __init__(self, _ErrorLocation = Tuple[str, int, int] -def create_errors(error_tuples: List[ErrorTuple]) -> List[MypyError]: - errors: List[MypyError] = [] - latest_error_at_location: Dict[_ErrorLocation, MypyError] = {} +def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: + errors: list[MypyError] = [] + latest_error_at_location: dict[_ErrorLocation, MypyError] = {} for error_tuple in error_tuples: file_path, line, column, severity, message, _, errorcode = error_tuple From a2bc04db02a78e4baa03f2f3b53230289db24d2f Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 00:04:02 +0530 Subject: [PATCH 10/37] Fix error tuple signature --- mypy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 1b8e685f47c8..34952c166095 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1232,7 +1232,7 @@ def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: latest_error_at_location: dict[_ErrorLocation, MypyError] = {} for error_tuple in error_tuples: - file_path, line, column, severity, message, _, errorcode = error_tuple + file_path, line, column, _, _, severity, message, _, errorcode = error_tuple if file_path is None: continue From 35974e4d0cd299e2341deaffb4c94521235c7f42 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 12:30:24 +0530 Subject: [PATCH 11/37] Import Optional --- mypy/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/build.py b/mypy/build.py index 61e7165a2a75..01cb86890ae7 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -36,6 +36,7 @@ Mapping, NamedTuple, NoReturn, + Optional, Sequence, TextIO, TypeVar, From 1e5ec912377f6428f6340dcbcfbbbb9d4dc71f58 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 13:12:32 +0530 Subject: [PATCH 12/37] Run black --- mypy/build.py | 7 +++---- mypy/error_formatter.py | 23 +++++++++++++---------- mypy/main.py | 3 ++- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 01cb86890ae7..5b8a7dd85d21 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -267,7 +267,7 @@ def _build( plugin=plugin, plugins_snapshot=snapshot, errors=errors, - error_formatter=JSONFormatter() if options.output == 'json' else None, + error_formatter=JSONFormatter() if options.output == "json" else None, flush_errors=flush_errors, fscache=fscache, stdout=stdout, @@ -619,7 +619,7 @@ def __init__( fscache: FileSystemCache, stdout: TextIO, stderr: TextIO, - error_formatter: Optional['ErrorFormatter'] = None, + error_formatter: Optional["ErrorFormatter"] = None, ) -> None: self.stats: dict[str, Any] = {} # Values are ints or floats self.stdout = stdout @@ -3444,8 +3444,7 @@ def process_stale_scc(graph: Graph, scc: list[str], manager: BuildManager) -> No for id in stale: graph[id].transitive_error = True for id in stale: - errors = manager.errors.file_messages( - graph[id].xpath, formatter=manager.error_formatter) + errors = manager.errors.file_messages(graph[id].xpath, formatter=manager.error_formatter) manager.flush_errors(errors, False) graph[id].write_cache() graph[id].mark_as_rechecked() diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 1cea67f7df31..7dec7a5c149e 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -8,18 +8,21 @@ class ErrorFormatter(ABC): """Defines how errors are formatted before being printed.""" + @abstractmethod - def report_error(self, error: 'MypyError') -> str: + def report_error(self, error: "MypyError") -> str: raise NotImplementedError class JSONFormatter(ErrorFormatter): - def report_error(self, error: 'MypyError') -> str: - return json.dumps({ - 'file': error.file_path, - 'line': error.line, - 'column': error.column, - 'message': error.message, - 'hint': error.hint, - 'code': None if error.errorcode is None else error.errorcode.code, - }) + def report_error(self, error: "MypyError") -> str: + return json.dumps( + { + "file": error.file_path, + "line": error.line, + "column": error.column, + "message": error.message, + "hint": error.hint, + "code": None if error.errorcode is None else error.errorcode.code, + } + ) diff --git a/mypy/main.py b/mypy/main.py index 5360decd62aa..3f587a0f058f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -521,7 +521,8 @@ def add_invertible_flag( ) general_group.add_argument( - '-O', '--output', metavar='FORMAT', help="Set a custom output format") + "-O", "--output", metavar="FORMAT", help="Set a custom output format" + ) config_group = parser.add_argument_group( title="Config file", From 723219f51632a2a78b19fc4876fc411f10bcc7a1 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 15:08:18 +0530 Subject: [PATCH 13/37] Run black on another file --- mypy/errors.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 34952c166095..d465b1966190 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1208,13 +1208,15 @@ def report_internal_error( class MypyError: - def __init__(self, - file_path: str, - line: int, - column: int, - message: str, - hint: str, - errorcode: Optional[ErrorCode]) -> None: + def __init__( + self, + file_path: str, + line: int, + column: int, + message: str, + hint: str, + errorcode: Optional[ErrorCode], + ) -> None: self.file_path = file_path self.line = line self.column = column @@ -1236,18 +1238,18 @@ def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: if file_path is None: continue - assert severity in ('error', 'note') - if severity == 'note': + assert severity in ("error", "note") + if severity == "note": error_location = (file_path, line, column) error = latest_error_at_location.get(error_location) if error is None: # No error tuple found for this hint. Ignoring it continue - if error.hint == '': + if error.hint == "": error.hint = message else: - error.hint += '\n' + message + error.hint += "\n" + message else: error = MypyError(file_path, line, column, message, "", errorcode) From 2228c0a42466a6a444ec6b94ba6594b41194d828 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 15:51:30 +0530 Subject: [PATCH 14/37] Run isort --- mypy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index d465b1966190..a8b318294fad 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -8,8 +8,8 @@ from typing_extensions import Final, Literal, TypeAlias as _TypeAlias from mypy import errorcodes as codes -from mypy.errorcodes import IMPORT, ErrorCode from mypy.error_formatter import ErrorFormatter +from mypy.errorcodes import IMPORT, ErrorCode from mypy.message_registry import ErrorMessage from mypy.options import Options from mypy.scope import Scope From 33d81b085222f84af40bb6b52a0d384e7cbcbdb7 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 23 Feb 2023 17:05:00 +0530 Subject: [PATCH 15/37] Run isort on build.py --- mypy/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index 5b8a7dd85d21..71a90f62fcf1 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,8 +47,8 @@ import mypy.semanal_main from mypy.checker import TypeChecker -from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error from mypy.error_formatter import ErrorFormatter, JSONFormatter +from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo From 1872ae6d222056962d9281436267702812a3a3ea Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 01:16:05 +0530 Subject: [PATCH 16/37] Add tests for json output --- mypy/error_formatter.py | 2 +- test-data/unit/check-output-json.test | 30 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test-data/unit/check-output-json.test diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 7dec7a5c149e..7fee1323e9ee 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -22,7 +22,7 @@ def report_error(self, error: "MypyError") -> str: "line": error.line, "column": error.column, "message": error.message, - "hint": error.hint, + "hint": error.hint or None, "code": None if error.errorcode is None else error.errorcode.code, } ) diff --git a/test-data/unit/check-output-json.test b/test-data/unit/check-output-json.test new file mode 100644 index 000000000000..fe721e948dd9 --- /dev/null +++ b/test-data/unit/check-output-json.test @@ -0,0 +1,30 @@ +-- Test cases for `--output=json` + +[case testOutputJsonSimple] +# flags: --output=json +def foo() -> None: + pass + +foo(1) +[out] +{"file": "main", "line": 5, "column": 0, "message": "Too many arguments for \"foo\"", "hint": null, "code": "call-arg"} + +[case testOutputJsonWithHint] +# flags: --output=json +from typing import Optional, overload + +@overload +def foo() -> None: ... +@overload +def foo(x: int) -> None: ... + +def foo(x: Optional[int] = None) -> None: + ... + +foo('42') + +def bar() -> None: ... +bar('42') +[out] +{"file": "main", "line": 12, "column": 0, "message": "No overload variant of \"foo\" matches argument type \"str\"", "hint": "Possible overload variants:\n def foo() -> None\n def foo(x: int) -> None", "code": "call-overload"} +{"file": "main", "line": 15, "column": 0, "message": "Too many arguments for \"bar\"", "hint": null, "code": "call-arg"} From 3abc9cb5bb84ddc7680a1a9d52d20ee79ceb4b0a Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 01:32:19 +0530 Subject: [PATCH 17/37] Suggestions from code review, and negative test --- mypy/error_formatter.py | 9 ++++++++- mypy/main.py | 15 ++++++++++++--- mypy/util.py | 9 ++++++++- test-data/unit/check-output-json.test | 8 ++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 7fee1323e9ee..451eb0e61956 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -1,3 +1,4 @@ +"""Defines the different custom formats in which mypy can output.""" import json from abc import ABC, abstractmethod from typing import TYPE_CHECKING @@ -7,7 +8,7 @@ class ErrorFormatter(ABC): - """Defines how errors are formatted before being printed.""" + """Base class to define how errors are formatted before being printed.""" @abstractmethod def report_error(self, error: "MypyError") -> str: @@ -15,7 +16,10 @@ def report_error(self, error: "MypyError") -> str: class JSONFormatter(ErrorFormatter): + """Formatter for basic JSON output format.""" + def report_error(self, error: "MypyError") -> str: + """Prints out the errors as simple, static JSON lines.""" return json.dumps( { "file": error.file_path, @@ -26,3 +30,6 @@ def report_error(self, error: "MypyError") -> str: "code": None if error.errorcode is None else error.errorcode.code, } ) + + +OUTPUT_CHOICES = {"json": JSONFormatter} diff --git a/mypy/main.py b/mypy/main.py index 88beb6b566ef..b521819ddc22 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -13,6 +13,7 @@ from mypy import build, defaults, state, util from mypy.config_parser import get_config_module_names, parse_config_file, parse_version +from mypy.error_formatter import OUTPUT_CHOICES from mypy.errorcodes import error_codes from mypy.errors import CompileError from mypy.find_sources import InvalidSourceList, create_source_list @@ -67,7 +68,9 @@ def main( if clean_exit: options.fast_exit = False - formatter = util.FancyFormatter(stdout, stderr, options.hide_error_codes) + formatter = util.FancyFormatter( + stdout, stderr, options.hide_error_codes, hide_success=options.output is not None + ) if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr): # Since --install-types performs user input, we want regular stdout and stderr. @@ -151,7 +154,9 @@ def run_build( stdout: TextIO, stderr: TextIO, ) -> tuple[build.BuildResult | None, list[str], bool]: - formatter = util.FancyFormatter(stdout, stderr, options.hide_error_codes) + formatter = util.FancyFormatter( + stdout, stderr, options.hide_error_codes, hide_success=options.output is not None + ) messages = [] @@ -521,7 +526,11 @@ def add_invertible_flag( ) general_group.add_argument( - "-O", "--output", metavar="FORMAT", help="Set a custom output format" + "-O", + "--output", + metavar="FORMAT", + help="Set a custom output format", + choices=OUTPUT_CHOICES, ) config_group = parser.add_argument_group( diff --git a/mypy/util.py b/mypy/util.py index 2c225c7fe651..5d7b57b95314 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -533,8 +533,12 @@ class FancyFormatter: This currently only works on Linux and Mac. """ - def __init__(self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool) -> None: + def __init__( + self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool, hide_success: bool = False + ) -> None: self.hide_error_codes = hide_error_codes + self.hide_success = hide_success + # Check if we are in a human-facing terminal on a supported platform. if sys.platform not in ("linux", "darwin", "win32", "emscripten"): self.dummy_term = True @@ -763,6 +767,9 @@ def format_success(self, n_sources: int, use_color: bool = True) -> str: n_sources is total number of files passed directly on command line, i.e. excluding stubs and followed imports. """ + if self.hide_success: + return "" + msg = f"Success: no issues found in {n_sources} source file{plural_s(n_sources)}" if not use_color: return msg diff --git a/test-data/unit/check-output-json.test b/test-data/unit/check-output-json.test index fe721e948dd9..f639c452dbe8 100644 --- a/test-data/unit/check-output-json.test +++ b/test-data/unit/check-output-json.test @@ -1,5 +1,13 @@ -- Test cases for `--output=json` +[case testOutputJsonNoIssues] +# flags: --output=json +def foo() -> None: + pass + +foo() +[out] + [case testOutputJsonSimple] # flags: --output=json def foo() -> None: From 6c9ab11e60265603495e7db6858c3f172b907c9b Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 01:42:30 +0530 Subject: [PATCH 18/37] Add default value of None --- mypy/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/main.py b/mypy/main.py index b521819ddc22..f306f43a3b0e 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -531,6 +531,7 @@ def add_invertible_flag( metavar="FORMAT", help="Set a custom output format", choices=OUTPUT_CHOICES, + default=None, ) config_group = parser.add_argument_group( From 627ed8eb353a02cf91cb9e7918695b38353d0b99 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 01:45:17 +0530 Subject: [PATCH 19/37] Default output to None in options as well --- mypy/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/options.py b/mypy/options.py index 08e4a78757c0..2775a55e9fcf 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -358,7 +358,7 @@ def __init__(self) -> None: self.force_union_syntax = False # Sets output format - self.output = "" + self.output = None def use_lowercase_names(self) -> bool: if self.python_version >= (3, 9): From e425cbe78ccb9a24fffbcca8109aa1aabc962333 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:31:09 +0530 Subject: [PATCH 20/37] Fix failing tests --- mypy/build.py | 4 ++-- mypy/main.py | 5 ++--- mypy/options.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 892cded99185..b353237b5826 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,7 +47,7 @@ import mypy.semanal_main from mypy.checker import TypeChecker -from mypy.error_formatter import ErrorFormatter, JSONFormatter +from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter, JSONFormatter from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder @@ -257,7 +257,7 @@ def _build( plugin=plugin, plugins_snapshot=snapshot, errors=errors, - error_formatter=JSONFormatter() if options.output == "json" else None, + error_formatter=OUTPUT_CHOICES.get(options.output), flush_errors=flush_errors, fscache=fscache, stdout=stdout, diff --git a/mypy/main.py b/mypy/main.py index f306f43a3b0e..f8e1ad7c6511 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -69,7 +69,7 @@ def main( options.fast_exit = False formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=options.output is not None + stdout, stderr, options.hide_error_codes, hide_success=options.output ) if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr): @@ -155,7 +155,7 @@ def run_build( stderr: TextIO, ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=options.output is not None + stdout, stderr, options.hide_error_codes, hide_success=options.output ) messages = [] @@ -531,7 +531,6 @@ def add_invertible_flag( metavar="FORMAT", help="Set a custom output format", choices=OUTPUT_CHOICES, - default=None, ) config_group = parser.add_argument_group( diff --git a/mypy/options.py b/mypy/options.py index 2775a55e9fcf..08e4a78757c0 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -358,7 +358,7 @@ def __init__(self) -> None: self.force_union_syntax = False # Sets output format - self.output = None + self.output = "" def use_lowercase_names(self) -> bool: if self.python_version >= (3, 9): From 47f1b078171bbb972c1efbd47130ce200d680668 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:31:20 +0530 Subject: [PATCH 21/37] improve docstring --- mypy/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/options.py b/mypy/options.py index 08e4a78757c0..f1c919b6b7ac 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -357,7 +357,7 @@ def __init__(self) -> None: self.force_uppercase_builtins = False self.force_union_syntax = False - # Sets output format + # Sets custom output format self.output = "" def use_lowercase_names(self) -> bool: From e00ad4a978d5b8714a31ff0dd5ce731ac4bff20a Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:48:09 +0530 Subject: [PATCH 22/37] type cast --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index f8e1ad7c6511..2f5f34c7707f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -69,7 +69,7 @@ def main( options.fast_exit = False formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=options.output + stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr): From c1fb6a21291c047be490dca474f677212124e0ee Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:48:41 +0530 Subject: [PATCH 23/37] Another explicit type cast --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 2f5f34c7707f..1bf087d02534 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -155,7 +155,7 @@ def run_build( stderr: TextIO, ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=options.output + stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) messages = [] From aafe3aa86635746142a10a1b6f7677b7d01a1f46 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:58:26 +0530 Subject: [PATCH 24/37] remove unused import --- mypy/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index b353237b5826..6c1218492346 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,7 +47,7 @@ import mypy.semanal_main from mypy.checker import TypeChecker -from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter, JSONFormatter +from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder From fae3215d9129f0a7e29fb8f994257898b665aaec Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 02:58:34 +0530 Subject: [PATCH 25/37] create formatter object --- mypy/error_formatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 451eb0e61956..d4b53c2e340f 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -32,4 +32,4 @@ def report_error(self, error: "MypyError") -> str: ) -OUTPUT_CHOICES = {"json": JSONFormatter} +OUTPUT_CHOICES = {"json": JSONFormatter()} From 89ad1d3f1505d5ed28e58e20ea2b0f4ea2ac6037 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 03:48:32 +0530 Subject: [PATCH 26/37] Add custom end to end test --- mypy/test/testoutput.py | 55 +++++++++++++++++++ ...check-output-json.test => outputjson.test} | 5 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 mypy/test/testoutput.py rename test-data/unit/{check-output-json.test => outputjson.test} (81%) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py new file mode 100644 index 000000000000..07d69fb9ca24 --- /dev/null +++ b/mypy/test/testoutput.py @@ -0,0 +1,55 @@ +"""Test cases for `--output=json`. + +These cannot be run by the usual unit test runner because of the backslashes in +the output, which get normalized to forward slashes by the test suite on Windows. +""" + +from __future__ import annotations + +import os +import os.path + +from mypy import api +from mypy.defaults import PYTHON3_VERSION +from mypy.test.config import test_temp_dir +from mypy.test.data import DataDrivenTestCase, DataSuite +from mypy.test.helpers import assert_string_arrays_equal + + +class OutputJSONsuite(DataSuite): + files = ["outputjson.test"] + + def run_case(self, testcase: DataDrivenTestCase) -> None: + test_output_json(testcase) + + +def test_output_json(testcase: DataDrivenTestCase) -> None: + """Runs Mypy in a subprocess, and ensures that `--output=json` works as intended.""" + mypy_cmdline = ["--output=json"] + mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}") + + # Write the program to a file. + program_path = os.path.join(test_temp_dir, "main") + mypy_cmdline.append(program_path) + with open(program_path, "w", encoding="utf8") as file: + for s in testcase.input: + file.write(f"{s}\n") + + output = [] + # Type check the program. + out, err, returncode = api.run(mypy_cmdline) + # split lines, remove newlines, and remove directory of test case + for line in (out + err).rstrip("\n").splitlines(): + if line.startswith(test_temp_dir + os.sep): + output.append(line[len(test_temp_dir + os.sep) :].rstrip("\r\n")) + else: + output.append(line.rstrip("\r\n")) + + if returncode > 1: + output.append("!!! Mypy crashed !!!") + + # Remove temp file. + os.remove(program_path) + + normalized_output = [line.replace(program_path, "main") for line in output] + assert normalized_output == testcase.output diff --git a/test-data/unit/check-output-json.test b/test-data/unit/outputjson.test similarity index 81% rename from test-data/unit/check-output-json.test rename to test-data/unit/outputjson.test index f639c452dbe8..b3de1b5a6179 100644 --- a/test-data/unit/check-output-json.test +++ b/test-data/unit/outputjson.test @@ -1,4 +1,7 @@ --- Test cases for `--output=json` +-- Test cases for `--output=json`. +-- These cannot be run by the usual unit test runner because of the backslashes +-- in the output, which get normalized to forward slashes by the test suite on +-- Windows. [case testOutputJsonNoIssues] # flags: --output=json From 7a3f7365c739320ff7dd6abcb4ac57f8ac26d7e6 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 04:04:06 +0530 Subject: [PATCH 27/37] unused import --- mypy/test/testoutput.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index 07d69fb9ca24..efe794b68d74 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -13,7 +13,6 @@ from mypy.defaults import PYTHON3_VERSION from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite -from mypy.test.helpers import assert_string_arrays_equal class OutputJSONsuite(DataSuite): From 6d46f75b09a33f79215d93fb9bbbaa5af4c64d60 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 04:19:23 +0530 Subject: [PATCH 28/37] trailing whitespace --- mypy/test/testoutput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index efe794b68d74..ca8a9f3a6c83 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -1,7 +1,7 @@ """Test cases for `--output=json`. These cannot be run by the usual unit test runner because of the backslashes in -the output, which get normalized to forward slashes by the test suite on Windows. +the output, which get normalized to forward slashes by the test suite on Windows. """ from __future__ import annotations From e71a372f0fdba35399ee2792e40a97abdb420386 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 10:37:05 +0530 Subject: [PATCH 29/37] try fixing windows --- mypy/test/testoutput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index ca8a9f3a6c83..dec28d06839d 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -50,5 +50,5 @@ def test_output_json(testcase: DataDrivenTestCase) -> None: # Remove temp file. os.remove(program_path) - normalized_output = [line.replace(program_path, "main") for line in output] + normalized_output = [line.replace(test_temp_dir + os.sep, "") for line in output] assert normalized_output == testcase.output From 8cca203abae0d58dc786684ec6564ab3621663c6 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 11:43:20 +0530 Subject: [PATCH 30/37] fix windows separator issue --- mypy/test/testoutput.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index dec28d06839d..af892f1d0aff 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import json import os import os.path @@ -50,5 +51,9 @@ def test_output_json(testcase: DataDrivenTestCase) -> None: # Remove temp file. os.remove(program_path) - normalized_output = [line.replace(test_temp_dir + os.sep, "") for line in output] + # JSON encodes every `\` character into `\\`, so we need to remove `\\` from windows paths + # and `/` from POSIX paths + json_os_separator = os.sep.replace("\\", "\\\\") + normalized_output = [line.replace(test_temp_dir + json_os_separator, "") for line in output] + assert normalized_output == testcase.output From 79e16a84a77a6b9124ac4501718b5656d721fe36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 06:13:58 +0000 Subject: [PATCH 31/37] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/test/testoutput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index af892f1d0aff..a10a933411d4 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -5,8 +5,8 @@ """ from __future__ import annotations -import json +import json import os import os.path From 8bf4890ff81bbec3d35cc73561e8340cce243ced Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 28 Apr 2023 12:27:55 +0530 Subject: [PATCH 32/37] unused import --- mypy/test/testoutput.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index a10a933411d4..41f6881658c8 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -6,7 +6,6 @@ from __future__ import annotations -import json import os import os.path From 0aafadf81bb9f2fd849515d59ad6b0f7eeb6abf8 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Wed, 10 May 2023 12:18:07 +0530 Subject: [PATCH 33/37] Pass error tuples to format_messages --- mypy/errors.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 7b6131d06b4c..0cbe3bb9e81f 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -778,7 +778,7 @@ def raise_error(self, use_stdout: bool = True) -> NoReturn: ) def format_messages( - self, error_info: list[ErrorInfo], source_lines: list[str] | None + self, error_tuples: list[ErrorTuple], source_lines: list[str] | None ) -> list[str]: """Return a string list that represents the error messages. @@ -787,9 +787,6 @@ def format_messages( severity 'error'). """ a: list[str] = [] - error_info = [info for info in error_info if not info.hidden] - errors = self.render_messages(self.sort_messages(error_info)) - errors = self.remove_duplicates(errors) for ( file, line, @@ -800,7 +797,7 @@ def format_messages( message, allow_dups, code, - ) in errors: + ) in error_tuples: s = "" if file is not None: if self.options.show_column_numbers and line >= 0 and column >= 0: @@ -854,11 +851,11 @@ def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) - return [] error_info = self.error_info_map[path] - if formatter is not None: - error_info = [info for info in error_info if not info.hidden] - error_tuples = self.render_messages(self.sort_messages(error_info)) - error_tuples = self.remove_duplicates(error_tuples) + error_info = [info for info in error_info if not info.hidden] + error_tuples = self.render_messages(self.sort_messages(error_info)) + error_tuples = self.remove_duplicates(error_tuples) + if formatter is not None: errors = create_errors(error_tuples) return [formatter.report_error(err) for err in errors] @@ -867,7 +864,7 @@ def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) - if self.options.pretty: assert self.read_source source_lines = self.read_source(path) - return self.format_messages(error_info, source_lines) + return self.format_messages(error_tuples, source_lines) def new_messages(self) -> list[str]: """Return a string list of new error messages. From e2fd45ed19c449b1a1566b80b8076f0e85f4202c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 20:54:53 +0000 Subject: [PATCH 34/37] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/build.py | 2 +- mypy/error_formatter.py | 1 + test-data/unit/outputjson.test | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 9b7c2b1fdefe..0812c33a9680 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -610,7 +610,7 @@ def __init__( fscache: FileSystemCache, stdout: TextIO, stderr: TextIO, - error_formatter: Optional["ErrorFormatter"] = None, + error_formatter: Optional[ErrorFormatter] = None, ) -> None: self.stats: dict[str, Any] = {} # Values are ints or floats self.stdout = stdout diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index d4b53c2e340f..3fed02488688 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -1,4 +1,5 @@ """Defines the different custom formats in which mypy can output.""" + import json from abc import ABC, abstractmethod from typing import TYPE_CHECKING diff --git a/test-data/unit/outputjson.test b/test-data/unit/outputjson.test index b3de1b5a6179..840a74427625 100644 --- a/test-data/unit/outputjson.test +++ b/test-data/unit/outputjson.test @@ -1,7 +1,7 @@ -- Test cases for `--output=json`. -- These cannot be run by the usual unit test runner because of the backslashes -- in the output, which get normalized to forward slashes by the test suite on --- Windows. +-- Windows. [case testOutputJsonNoIssues] # flags: --output=json From 4b03c5c508cc6beb693e1b1fd9df396e1f2d598a Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 10 May 2024 02:27:57 +0530 Subject: [PATCH 35/37] ruff lints --- mypy/build.py | 3 +-- mypy/errors.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 0812c33a9680..6bfb6f3f6482 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -37,7 +37,6 @@ Mapping, NamedTuple, NoReturn, - Optional, Sequence, TextIO, ) @@ -610,7 +609,7 @@ def __init__( fscache: FileSystemCache, stdout: TextIO, stderr: TextIO, - error_formatter: Optional[ErrorFormatter] = None, + error_formatter: ErrorFormatter | None = None, ) -> None: self.stats: dict[str, Any] = {} # Values are ints or floats self.stdout = stdout diff --git a/mypy/errors.py b/mypy/errors.py index 2d216f0553ae..73d948463820 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -899,7 +899,7 @@ def format_messages( a.append(" " * (DEFAULT_SOURCE_OFFSET + column) + marker) return a - def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) -> list[str]: + def file_messages(self, path: str, formatter: ErrorFormatter | None = None) -> list[str]: """Return a string list of new error messages from a given file. Use a form suitable for displaying to the user. @@ -1296,7 +1296,7 @@ def __init__( column: int, message: str, hint: str, - errorcode: Optional[ErrorCode], + errorcode: ErrorCode | None, ) -> None: self.file_path = file_path self.line = line From e0e68962251249689c8694896539e0a6b1fed685 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Fri, 10 May 2024 15:22:32 +0530 Subject: [PATCH 36/37] address comments --- mypy/build.py | 2 +- mypy/error_formatter.py | 3 ++- mypy/errors.py | 16 ++++++++-------- mypy/options.py | 2 +- test-data/unit/outputjson.test | 9 ++++++--- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 6bfb6f3f6482..3ceb473f0948 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -254,7 +254,7 @@ def _build( plugin=plugin, plugins_snapshot=snapshot, errors=errors, - error_formatter=OUTPUT_CHOICES.get(options.output), + error_formatter=None if options.output is None else OUTPUT_CHOICES.get(options.output), flush_errors=flush_errors, fscache=fscache, stdout=stdout, diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index 3fed02488688..a9a95978f94a 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -27,8 +27,9 @@ def report_error(self, error: "MypyError") -> str: "line": error.line, "column": error.column, "message": error.message, - "hint": error.hint or None, + "hint": None if len(error.hints) == 0 else "\n".join(error.hints), "code": None if error.errorcode is None else error.errorcode.code, + "is_note": error.is_note, } ) diff --git a/mypy/errors.py b/mypy/errors.py index 73d948463820..9f1665fedc31 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1295,15 +1295,16 @@ def __init__( line: int, column: int, message: str, - hint: str, errorcode: ErrorCode | None, + is_note: bool = False, ) -> None: self.file_path = file_path self.line = line self.column = column self.message = message - self.hint = hint self.errorcode = errorcode + self.is_note = is_note + self.hints: list[str] = [] # (file_path, line, column) @@ -1324,16 +1325,15 @@ def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: error_location = (file_path, line, column) error = latest_error_at_location.get(error_location) if error is None: - # No error tuple found for this hint. Ignoring it + # This is purely a note, with no error correlated to it + error = MypyError(file_path, line, column, message, errorcode, is_note=True) + errors.append(error) continue - if error.hint == "": - error.hint = message - else: - error.hint += "\n" + message + error.hints.append(message) else: - error = MypyError(file_path, line, column, message, "", errorcode) + error = MypyError(file_path, line, column, message, errorcode) errors.append(error) error_location = (file_path, line, column) latest_error_at_location[error_location] = error diff --git a/mypy/options.py b/mypy/options.py index 39de067418ae..91639828801e 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -380,7 +380,7 @@ def __init__(self) -> None: self.force_union_syntax = False # Sets custom output format - self.output = "" + self.output: str | None = None def use_lowercase_names(self) -> bool: if self.python_version >= (3, 9): diff --git a/test-data/unit/outputjson.test b/test-data/unit/outputjson.test index 840a74427625..72d36a50415d 100644 --- a/test-data/unit/outputjson.test +++ b/test-data/unit/outputjson.test @@ -18,7 +18,7 @@ def foo() -> None: foo(1) [out] -{"file": "main", "line": 5, "column": 0, "message": "Too many arguments for \"foo\"", "hint": null, "code": "call-arg"} +{"file": "main", "line": 5, "column": 0, "message": "Too many arguments for \"foo\"", "hint": null, "code": "call-arg", "is_note": false} [case testOutputJsonWithHint] # flags: --output=json @@ -32,10 +32,13 @@ def foo(x: int) -> None: ... def foo(x: Optional[int] = None) -> None: ... +reveal_type(foo) + foo('42') def bar() -> None: ... bar('42') [out] -{"file": "main", "line": 12, "column": 0, "message": "No overload variant of \"foo\" matches argument type \"str\"", "hint": "Possible overload variants:\n def foo() -> None\n def foo(x: int) -> None", "code": "call-overload"} -{"file": "main", "line": 15, "column": 0, "message": "Too many arguments for \"bar\"", "hint": null, "code": "call-arg"} +{"file": "main", "line": 12, "column": 12, "message": "Revealed type is \"Overload(def (), def (x: builtins.int))\"", "hint": null, "code": "misc", "is_note": true} +{"file": "main", "line": 14, "column": 0, "message": "No overload variant of \"foo\" matches argument type \"str\"", "hint": "Possible overload variants:\n def foo() -> None\n def foo(x: int) -> None", "code": "call-overload", "is_note": false} +{"file": "main", "line": 17, "column": 0, "message": "Too many arguments for \"bar\"", "hint": null, "code": "call-arg", "is_note": false} From a0dc6d194fa55ab846a40a5d10fce86fedc3f08b Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Sat, 11 May 2024 00:18:20 +0530 Subject: [PATCH 37/37] use severity --- mypy/error_formatter.py | 2 +- mypy/errors.py | 8 ++++---- test-data/unit/outputjson.test | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index a9a95978f94a..ffc6b6747596 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -29,7 +29,7 @@ def report_error(self, error: "MypyError") -> str: "message": error.message, "hint": None if len(error.hints) == 0 else "\n".join(error.hints), "code": None if error.errorcode is None else error.errorcode.code, - "is_note": error.is_note, + "severity": error.severity, } ) diff --git a/mypy/errors.py b/mypy/errors.py index 9f1665fedc31..7a937da39c20 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1296,14 +1296,14 @@ def __init__( column: int, message: str, errorcode: ErrorCode | None, - is_note: bool = False, + severity: Literal["error", "note"], ) -> None: self.file_path = file_path self.line = line self.column = column self.message = message self.errorcode = errorcode - self.is_note = is_note + self.severity = severity self.hints: list[str] = [] @@ -1326,14 +1326,14 @@ def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: error = latest_error_at_location.get(error_location) if error is None: # This is purely a note, with no error correlated to it - error = MypyError(file_path, line, column, message, errorcode, is_note=True) + error = MypyError(file_path, line, column, message, errorcode, severity="note") errors.append(error) continue error.hints.append(message) else: - error = MypyError(file_path, line, column, message, errorcode) + error = MypyError(file_path, line, column, message, errorcode, severity="error") errors.append(error) error_location = (file_path, line, column) latest_error_at_location[error_location] = error diff --git a/test-data/unit/outputjson.test b/test-data/unit/outputjson.test index 72d36a50415d..43649b7b781d 100644 --- a/test-data/unit/outputjson.test +++ b/test-data/unit/outputjson.test @@ -18,7 +18,7 @@ def foo() -> None: foo(1) [out] -{"file": "main", "line": 5, "column": 0, "message": "Too many arguments for \"foo\"", "hint": null, "code": "call-arg", "is_note": false} +{"file": "main", "line": 5, "column": 0, "message": "Too many arguments for \"foo\"", "hint": null, "code": "call-arg", "severity": "error"} [case testOutputJsonWithHint] # flags: --output=json @@ -39,6 +39,6 @@ foo('42') def bar() -> None: ... bar('42') [out] -{"file": "main", "line": 12, "column": 12, "message": "Revealed type is \"Overload(def (), def (x: builtins.int))\"", "hint": null, "code": "misc", "is_note": true} -{"file": "main", "line": 14, "column": 0, "message": "No overload variant of \"foo\" matches argument type \"str\"", "hint": "Possible overload variants:\n def foo() -> None\n def foo(x: int) -> None", "code": "call-overload", "is_note": false} -{"file": "main", "line": 17, "column": 0, "message": "Too many arguments for \"bar\"", "hint": null, "code": "call-arg", "is_note": false} +{"file": "main", "line": 12, "column": 12, "message": "Revealed type is \"Overload(def (), def (x: builtins.int))\"", "hint": null, "code": "misc", "severity": "note"} +{"file": "main", "line": 14, "column": 0, "message": "No overload variant of \"foo\" matches argument type \"str\"", "hint": "Possible overload variants:\n def foo() -> None\n def foo(x: int) -> None", "code": "call-overload", "severity": "error"} +{"file": "main", "line": 17, "column": 0, "message": "Too many arguments for \"bar\"", "hint": null, "code": "call-arg", "severity": "error"}