-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Flush error messages incrementally after processing a file #4396
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
Changes from 1 commit
4b38029
76f945f
310611d
059de14
c9a5a96
76bb7f8
5c9abec
844d5ca
c9d2355
699ba0b
376982d
caa8477
92dfc2d
cc64198
548078c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,9 +25,10 @@ | |
import time | ||
from os.path import dirname, basename | ||
import errno | ||
from functools import wraps | ||
|
||
from typing import (AbstractSet, Any, cast, Dict, Iterable, Iterator, List, | ||
Mapping, NamedTuple, Optional, Set, Tuple, Union, Callable) | ||
Mapping, NamedTuple, Optional, Set, Tuple, TypeVar, Union, Callable) | ||
# Can't use TYPE_CHECKING because it's not in the Python 3.5.1 stdlib | ||
MYPY = False | ||
if MYPY: | ||
|
@@ -127,13 +128,32 @@ def is_source(self, file: MypyFile) -> bool: | |
# be updated in place with newly computed cache data. See dmypy.py. | ||
SavedCache = Dict[str, Tuple['CacheMeta', MypyFile, Dict[Expression, Type]]] | ||
|
||
F = TypeVar('F', bound=Callable[..., Any]) | ||
|
||
|
||
def flush_compile_errors(f: F) -> F: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest not to make this a decorator at all, but a wrapper function. You could rename |
||
"""Catch and flush out any messages from a CompileError thrown in build.""" | ||
@wraps(f) | ||
def func(*args, **kwargs): | ||
# type: (*Any, **Any) -> Any | ||
try: | ||
return f(*args, **kwargs) | ||
except CompileError as e: | ||
serious = not e.use_stdout | ||
error_flush = kwargs.get('flush_errors', None) | ||
if error_flush: | ||
error_flush(e.messages[e.num_already_seen:], serious) | ||
raise | ||
return cast(F, func) | ||
|
||
|
||
@flush_compile_errors | ||
def build(sources: List[BuildSource], | ||
options: Options, | ||
alt_lib_path: Optional[str] = None, | ||
bin_dir: Optional[str] = None, | ||
saved_cache: Optional[SavedCache] = None, | ||
flush_errors: Optional[Callable[[List[str]], None]] = None, | ||
flush_errors: Optional[Callable[[List[str], bool], None]] = None, | ||
plugin: Optional[Plugin] = None, | ||
) -> BuildResult: | ||
"""Analyze a program. | ||
|
@@ -539,7 +559,7 @@ def __init__(self, data_dir: str, | |
version_id: str, | ||
plugin: Plugin, | ||
errors: Errors, | ||
flush_errors: Optional[Callable[[List[str]], None]] = None, | ||
flush_errors: Optional[Callable[[List[str], bool], None]] = None, | ||
saved_cache: Optional[SavedCache] = None, | ||
) -> None: | ||
self.start_time = time.time() | ||
|
@@ -712,9 +732,9 @@ def add_stats(self, **kwds: Any) -> None: | |
def stats_summary(self) -> Mapping[str, object]: | ||
return self.stats | ||
|
||
def error_flush(self, msgs: List[str]) -> None: | ||
def error_flush(self, msgs: List[str], serious: bool=False) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider getting rid of this and just inlining the only call site to add serious=False? |
||
if self.flush_errors: | ||
self.flush_errors(msgs) | ||
self.flush_errors(msgs, serious) | ||
|
||
|
||
def remove_cwd_prefix_from_path(p: str) -> str: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,28 +61,23 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: | |
if args is None: | ||
args = sys.argv[1:] | ||
sources, options = process_options(args) | ||
serious = False | ||
errors = False | ||
|
||
def flush_errors(a: List[str]) -> None: | ||
nonlocal errors | ||
if a: | ||
errors = True | ||
def flush_errors(a: List[str], serious: bool) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you rename |
||
f = sys.stderr if serious else sys.stdout | ||
try: | ||
for m in a: | ||
f.write(m + '\n') | ||
except BrokenPipeError: | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd |
||
|
||
serious = False | ||
try: | ||
res = type_check_only(sources, bin_dir, options, flush_errors) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer if the main body of test cases ( |
||
a = res.errors | ||
except CompileError as e: | ||
a = e.messages | ||
if not e.use_stdout: | ||
serious = True | ||
flush_errors(a[e.num_already_seen:]) | ||
if options.warn_unused_configs and options.unused_configs: | ||
print("Warning: unused section(s) in %s: %s" % | ||
(options.config_file, | ||
|
@@ -91,7 +86,7 @@ def flush_errors(a: List[str]) -> None: | |
if options.junit_xml: | ||
t1 = time.time() | ||
util.write_junit_xml(t1 - t0, serious, a, options.junit_xml) | ||
if errors: | ||
if a: | ||
9E88 | sys.exit(1) | |
|
||
|
||
|
@@ -121,7 +116,7 @@ def readlinkabs(link: str) -> str: | |
|
||
def type_check_only(sources: List[BuildSource], bin_dir: Optional[str], | ||
options: Options, | ||
flush_errors: Optional[Callable[[List[str]], None]]) -> BuildResult: | ||
flush_errors: Optional[Callable[[List[str], bool], None]]) -> BuildResult: | ||
# Type-check the program and dependencies. | ||
return build.build(sources=sources, | ||
bin_dir=bin_dir, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,36 +31,42 @@ def test_error_stream(testcase: DataDrivenTestCase) -> None: | |
options = Options() | ||
options.show_traceback = True | ||
|
||
a = [] | ||
logged_messages: List[str] = [] | ||
real_messages: List[str] = [] | ||
|
||
def flush_errors(msgs: List[str]) -> None: | ||
nonlocal a | ||
def flush_errors(msgs: List[str], serious: bool, is_real: bool=True) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be spaces around the |
||
if msgs: | ||
a.append('==== Errors flushed ====') | ||
a += msgs | ||
logged_messages.append('==== Errors flushed ====') | ||
logged_messages.extend(msgs) | ||
if is_real: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block can also be indented. |
||
real_messages.extend(msgs) | ||
|
||
plugin = ChainedPlugin(options, [LoggingPlugin(options, flush_errors), DefaultPlugin(options)]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style nit: Add empty line after nested function for clarity. |
||
|
||
sources = [BuildSource('main', '__main__', '\n'.join(testcase.input))] | ||
try: | ||
build.build(sources=sources, | ||
options=options, | ||
alt_lib_path=test_temp_dir, | ||
flush_errors=flush_errors, | ||
plugin=plugin) | ||
res = build.build(sources=sources, | ||
options=options, | ||
alt_lib_path=test_temp_dir, | ||
flush_errors=flush_errors, | ||
plugin=plugin) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only place where we pass a plugin to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another approach would be arrange to load a plugin through the regular mechanisms---that requires the plugin communicating with the test via global state, though. Another another approach is to ditch the plugin because the possible failure tests for (errors being streamed out properly but not until all typing is done) seems pretty marginal anyways. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay with ditching the plugin. |
||
reported_messages = res.errors | ||
except CompileError as e: | ||
a.append('==== Blocking error ====') | ||
a += e.messages[e.num_already_seen:] | ||
reported_messages = e.messages | ||
|
||
assert_string_arrays_equal(testcase.output, a, | ||
assert_string_arrays_equal(testcase.output, logged_messages, | ||
'Invalid output ({}, line {})'.format( | ||
testcase.file, testcase.line)) | ||
assert_string_arrays_equal(reported_messages, real_messages, | ||
'Streamed/reported mismatch ({}, line {})'.format( | ||
testcase.file, testcase.line)) | ||
|
||
|
||
# Use a typechecking plugin to allow test cases to emit messages | ||
# during typechecking. This allows us to verify that error messages | ||
# from one SCC are printed before later ones are typechecked. | ||
class LoggingPlugin(Plugin): | ||
def __init__(self, options: Options, log: Callable[[List[str]], None]) -> None: | ||
def __init__(self, options: Options, log: Callable[[List[str], bool, bool], None]) -> None: | ||
super().__init__(options) | ||
self.log = log | ||
|
||
|
@@ -72,5 +78,5 @@ def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext | |
def hook(self, ctx: FunctionContext) -> Type: | ||
assert(isinstance(ctx.context, CallExpr) and len(ctx.context.args) > 0 and | ||
isinstance(ctx.context.args[0], StrExpr)) | ||
self.log([ctx.context.args[0].value]) | ||
self.log([ctx.context.args[0].value], False, False) | ||
return ctx.default_return_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need this any more.