-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Show code snippets on demand #7440
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
Conversation
I just discovered that this doesn't work well with blocking errors and/or daemon. I have a fix but it requires |
Some general comments, most of which are copied from #7411: I think that we should fix at least the most obvious problems with column numbers before documenting this as a feature (it's okay to have it as a hidden option though). I have a half-finished PR that fixes column numbers of arguments and may be able to finish that up soon. Also, assignments should probably point to the rvalue. Other comments:
|
Interesting, why does is need |
I actually like the new position more, since we are already taking much more vertical space, why not save a bit of horizontal space? Otherwise the line with wiggly arrow will have few information on it.
It is hard to define what is "token". It seems to me you don't realize how hard would be implementing this to a reasonable degree of reliability and how misleading the positions can be if we will do this in ad-hoc manner. Like we may end up underlining the first item of a tuple while the problem is actually in the type of the second one. I would rather focus on making the start column better first.
OK.
OK.
I want to pass the cache as an argument to the |
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.
Here is a more detailed review of the code (my previous comments were about the formatting of the output). I'm excited that this will be improve usability by making it easier to figure out why mypy is complaining about something.
mypy/errors.py
Outdated
decode_python_encoding, DecodeError, trim_source_line, DEFAULT_SOURCE_OFFSET, | ||
WIGGLY_LINE, DEFAULT_COLUMNS, MINIMUM_WIDTH | ||
) | ||
from mypy.fscache import FileSystemCache |
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.
I think it would be nice if there was no import from mypy.fscache
in this file, and instead we'd accept a callback that reads file contents, for example (dependency injection). We could probably also get rid of the decode_python_encoding
and DecodeError
imports.
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.
OK, I was thinking about a protocol, but callback is even better.
mypy/errors.py
Outdated
self.show_column_numbers = show_column_numbers | ||
self.show_error_codes = show_error_codes | ||
def __init__(self, fscache: Optional[FileSystemCache] = None, | ||
options: Optional[Options] = None) -> None: |
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.
This adds a big dependency, which we previously avoided by providing the required information explicitly. It would arguably be cleaner if you'd just add the required options as arguments here instead of passing the whole Options
object.
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.
OK, I will split this back.
mypy/errors.py
Outdated
source_lines = None | ||
if self.options and self.options.show_source_code: | ||
if self.fscache: | ||
try: |
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.
This try statement could be provided as a callback to avoid a depending on fscache
.
@@ -20,6 +21,15 @@ | |||
ENCODING_RE = \ | |||
re.compile(br'([ \t\v]*#.*(\r\n?|\n))??[ \t\v]*#.*coding[:=][ \t]*([-\w.]+)') # type: Final | |||
|
|||
PLAIN_ANSI_DIM = '\x1b[2m' # type: Final |
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.
Is this portable? Could we get this from curses
or something?
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.
I tried it in couple terminals and it works (because it is ANSI standard). The problem is that although it is a basic ANSI "feature", terminfo files for most default terminals don't have dim
termcap entry, so curses
doesn't report it. I was thinking about choosing a grey color that would look good on both white and black background, but it is actually not easy, and again most default terminals are 8-color, not 256-color, so we can't get the color code from curses
.
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.
Ok, sounds reasonable. Maybe add a comment about this?
Since this is not on by default, we can iterate on this afterwards even if some things don't work in every possible environment.
@@ -110,6 +119,24 @@ def decode_python_encoding(source: bytes, pyversion: Tuple[int, int]) -> str: | |||
return source_text | |||
|
|||
|
|||
def trim_source_line(line: str, max_len: int, col: int, min_width: int) -> Tuple[str, int]: |
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.
This function is a great candidate for regular unit tests.
mypy/util.py
Outdated
return start + self.colors[color] + text + self.NORMAL | ||
|
||
def colorize(self, error: str) -> str: | ||
"""Colorize an output line by highlighting the status and error code.""" | ||
if ': error:' in error: | ||
loc, msg = error.split('error:', maxsplit=1) | ||
if not self.show_error_codes: | ||
if self.show_source_code: | ||
# Improve readability by wrapping lines when showing source code. |
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.
It's can be surprising that showing source code also affects how long messages are wrapped. Maybe the option should be named --pretty
or something, as it controls a few things.
mypy/util.py
Outdated
@@ -401,10 +492,18 @@ def colorize(self, error: str) -> str: | |||
elif ': note:' in error: | |||
loc, msg = error.split('note:', maxsplit=1) | |||
return loc + self.style('note:', 'blue') + self.underline_link(msg) | |||
elif self.show_source_code and error.startswith(' ' * DEFAULT_SOURCE_OFFSET): |
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.
Detecting source code highlights through an indent prefix can be surprising. I wonder if there's a more general way to do this. If there's no alternative simple approach, this should be documented carefully somewhere.
mypy/util.py
Outdated
return res | ||
|
||
|
||
def get_term_columns() -> int: |
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.
What about renaming this to get_terminal_width
?
mypy/errors.py
Outdated
if add_snippets: | ||
if severity == 'error' and source_lines and line > 0: | ||
source_line, offset = trim_source_line(source_lines[line - 1], | ||
DEFAULT_COLUMNS, column, MINIMUM_WIDTH) |
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.
Should this use the terminal width?
mypy/util.py
Outdated
if self.show_source_code: | ||
# Improve readability by wrapping lines when showing source code. | ||
pad = len(loc) + len('error: ') | ||
max_len = get_term_columns() - pad - 1 # compensate for space after 'error:' |
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.
This is kind of suspicious -- how will this work in the daemon? Should the client pass the terminal width to the server as part of each request, since the terminal may change between invocations?
Anyway, it might be better if this was provided by the caller.
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.
This is kind of suspicious -- how will this work in the daemon?
I was going to fix this after the other PR lands. Essentially yes, this should be passed together with is_tty
from the other PR.
@JukkaL thanks for review, I agree with all comments I didn't reply above. |
I think it's okay that the wiggly arrow is on its own line without anything else. The savings in horizontal space will be pretty minor, since error codes tend to be short. Here's some reasoning why I'd rather not add the error code after the wiggly arrow (besides that it subjectively looks odd to me):
Yes, there is a risk of doing something confusing. However, I think that the current approach is also confusing sometimes, especially if the error is at the end of the line, or if the target is a name that is much longer/shorter than the squiggle. I initially thought that it underlines a span of code, and was confused when this wasn't the case. A conservative option would be to only show a caret ( Here are other heuristics that could be built on top of the "caret only" approach that seem reasonably safe bets to me (but I haven't thought about this very carefully):
As long as we can't always find the end span of an AST node, we may want to allow some extra options to be passed along with error messages that may affect how the length of the underlining is determined. For example, if the target expression is an attribute expression |
Thanks for the updates! I like how it looks now. |
Wow! This looks really good. Thank you for working on it Ivan! |
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.
Looks great! There doesn't seem to be any tests for the dmypy client change -- maybe add at least one? Even if the change is trivial, it might get more complex in the future.
"Long[Type, Names]" are never split. | ||
^^^^-------------------------------------------------- | ||
num_indent max_len | ||
""" |
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.
I like the detailed comment!
This essentially fixes #7411
Couple ideas I think may make the much larger amount of output more readable are:
An example output (dark terminal on Linux):