Skip to content
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

Disable 80 col hard wrapping in mypy output #16488

Merged
merged 11 commits into from
Aug 19, 2022

Conversation

huonw
Copy link
Contributor

@huonw huonw commented Aug 12, 2022

This fixes part of #16451 by ensuring that the mypy output isn't truncated/hard-wrapped at all. In particular: the terminal width is forcibly set to be very wide ("infinite") because soft-wrapped display in a wide terminal is nicer than it being hard-wrapped/truncated to 80 cols.

This seems to only be possible via an undocumented environment variable: MYPY_FORCE_TERMINAL_WIDTH: https://github.com/python/mypy/blob/03638dd670373db0b8f00cc3bcec256d09729d06/mypy/util.py#L468

@@ -319,6 +321,8 @@ async def mypy_typecheck_partition(
env = {
"PEX_EXTRA_SYS_PATH": ":".join(all_used_source_roots),
"MYPYPATH": ":".join(all_used_source_roots),
"MYPY_FORCE_COLOR": "1" if global_options.colors else "0",
"MYPY_FORCE_TERMINAL_WIDTH": str(shutil.get_terminal_size().columns),
Copy link
Contributor Author

@huonw huonw Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear if this is appropriate in two ways:

  • will this ever be running in the pantsd, where it may not have access to the real tty (and/or have an outdated understanding of the size)?
  • I note that there's a pants.util.docutil.terminal_width function, but that seems to be targeted at help text? Should I be using it here?
    def terminal_width(*, fallback: int = 96, padding: int = 2) -> int:
    return shutil.get_terminal_size(fallback=(fallback, 24)).columns - padding

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pantsd propagates the TTY, so this should work as is.

But there is another thing to consider here, which is that including this information makes the cache key of the process dependent on your terminal width and the value of the --color flag, which isn't ideal.

For color in particular, we apparently do the same thing for pytest... which is also not great. Instead, what would likely be better to do for color in particular (for both pytest and mypy) would be to always request it in the consuming process, but then strip it (via e.g. https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python) from stdout/stderr afterwards before creating the CheckResult.

Terminal width is a separate issue: it cannot be adjusted post-facto. One option would be to hardcode it to e.g. 100 based on a global option (so that folks could fiddle with it in case the default wasn't good). Another option (which would be more magical but maybe totally fine) would be to round the terminal width down to the nearest 10 or 20 (or to 80 as a minimum) so that everyone with a terminal width within 10 or 20 characters of one another could still hit the cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's a real bummer on terminal width...it's going to be a barrier to "desktop caching" working well, where remote cache can be shared amongst several users.

I like your suggestion for post-processing colors for both MyPy and Pytest.

Copy link
Member

@stuhood stuhood Aug 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's a real bummer on terminal width...it's going to be a barrier to "desktop caching" working well, where remote cache can be shared amongst several users.

With a sufficiently granular rounding threshold (probably more like down to the nearest 40 or maybe even 80 characters), the hitrate would still be good. It's just annoyingly magical.

Copy link
Contributor

@Eric-Arellano Eric-Arellano Aug 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, even for local caching I'm not sure about it. If I expand my terminal because I went from 13" laptop to ultrawide monitor, I would not expect MyPy to be invalidated. I actually make that change a lot!

Perhaps we don't want to land the terminal width change yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the discussion.

I'll make the change to mypy to set MYPY_FORCE_COLOR=1 always, and then call colors.strip_color (but let me know if you'd prefer pants to write its own form) or not, depending on the colour option. To keep this PR focused (since I have limited time to work on it 😄 ), I won't touch the pytest code here.

For terminal width, one option might be to hardcode to 'infinity' (9999999 or something), essentially disabling mypy's hard-wrapping entirely by default. This won't work for pytest, but should work for mypy: I'll do some experiments to confirm.

  1. the terminal can soft-wrap diagnostic text just fine
  2. source code typically is typically natively hard-wrapped to some manageable limit already (80, 100, 120 characters)

IIRC, this is more along the lines of what other compilers do anyway (at least 1), e.g. rustc in a 60 char terminal:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the infinity approach is really promising, as long as the terminal indeed will do soft wrapping. Imo, that's preferable to a global option for MyPy, which is annoying to ask the user to set and still breaks caching. Thanks!

Sounds good re: Pytest, I'll open a ticket.

Copy link
Contributor Author

@huonw huonw Aug 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this along those lines. I think the output is acceptable, even for very wide code (200+ cols) in a narrow terminal:

image

A few notes for this new version:

  1. I've put the color stripping into CheckResult.from_fallible_process_result, so it can be used for other checks too. I imagine this could be used in LintResult and even TestResult but it kinda seems like there's deeper sharing between those types so I haven't tried to merge them here.
  2. I've had to set TERM too to get colors (mypy uses curses to do it 'properly' rather than the rather more convenient approach of hardcoding the escapes ala ansicolors). From skimming https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/8 and https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/11, it seemed like TERM=xterm was a reasonable lowest common denominator for terminal emulates used in practice (especially for the simple escape sequences mypy uses https://github.com/python/mypy/blob/9a9bc3b807bb11986122dd3896e6eb3b61e2ccf8/mypy/util.py#L626-L633), but I don't have much insight here, so please let me know if there's a preferred choice.
  3. I still haven't written any tests for the actual mypy invocations, assuming the code looks reasonable, I can add something to rules_integration_test.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still haven't written any tests for the actual mypy invocations, assuming the code looks reasonable, I can add something to rules_integration_test.py

Ah, looks like I have to update it anyway. It'll probably have to wait to tomorrow on my end, though.

Copy link
Contributor Author

@huonw huonw Aug 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seemed like TERM=xterm was a reasonable lowest common denominator for terminal emulates used in practice (especially for the simple escape sequences mypy uses https://github.com/python/mypy/blob/9a9bc3b807bb11986122dd3896e6eb3b61e2ccf8/mypy/util.py#L626-L633), but I don't have much insight here, so please let me know if there's a preferred choice

Ah, unfortunately this didn't work: for xterm, terminfo (at least on my macOS system) uses a more complicated escape sequence for sgr0 (reset) \x1b(B\x1b[m, instead of just some variant of the normal \x1b[0m. colors.strip_color doesn't handle the \x1b(B escape, nor do any of the regexps in the StackOverflow discussion above.

Instead of trying to patch holes like this or pull in a library like https://github.com/selectel/pyte to do it fully properly, I've just switched to running mypy with a simpler terminal emulator specified (TERM=linux).

TERM=xterm python -c 'import curses; curses.setupterm(fd=1); print(repr(curses.tigetstr("sgr0").decode()))'
# '\x1b(B\x1b[m'

TERM=linux python -c 'import curses; curses.setupterm(fd=1); print(repr(curses.tigetstr("sgr0").decode()))'
# '\x1b[0;10m'

I've also added a basic smoke test since this took me so long to actually get working properly 😅

# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
Copy link
Member

@stuhood stuhood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the contribution!

@@ -319,6 +321,8 @@ async def mypy_typecheck_partition(
env = {
"PEX_EXTRA_SYS_PATH": ":".join(all_used_source_roots),
"MYPYPATH": ":".join(all_used_source_roots),
"MYPY_FORCE_COLOR": "1" if global_options.colors else "0",
"MYPY_FORCE_TERMINAL_WIDTH": str(shutil.get_terminal_size().columns),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pantsd propagates the TTY, so this should work as is.

But there is another thing to consider here, which is that including this information makes the cache key of the process dependent on your terminal width and the value of the --color flag, which isn't ideal.

For color in particular, we apparently do the same thing for pytest... which is also not great. Instead, what would likely be better to do for color in particular (for both pytest and mypy) would be to always request it in the consuming process, but then strip it (via e.g. https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python) from stdout/stderr afterwards before creating the CheckResult.

Terminal width is a separate issue: it cannot be adjusted post-facto. One option would be to hardcode it to e.g. 100 based on a global option (so that folks could fiddle with it in case the default wasn't good). Another option (which would be more magical but maybe totally fine) would be to round the terminal width down to the nearest 10 or 20 (or to 80 as a minimum) so that everyone with a terminal width within 10 or 20 characters of one another could still hit the cache.

huonw added 3 commits August 16, 2022 11:59
[ci skip-rust]

[ci skip-build-wheels]
[ci skip-rust]

[ci skip-build-wheels]
Copy link
Member

@stuhood stuhood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

[ci skip-rust]

[ci skip-build-wheels]
Copy link
Contributor

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent PR: meaningful user impact, thoughtful comments, effective testing. Well done and thank you!

@benjyw
Copy link
Contributor

benjyw commented Aug 16, 2022

Great change, thanks for this!

@stuhood
Copy link
Member

stuhood commented Aug 16, 2022

Thanks again! It looks like this needs one more test fix.

@huonw huonw changed the title Propagate color/terminal width to mypy Show colors and disable line wrapping in mypy output Aug 18, 2022
@stuhood stuhood enabled auto-merge (squash) August 18, 2022 03:25
@stuhood stuhood disabled auto-merge August 18, 2022 03:25
@stuhood stuhood enabled auto-merge (squash) August 18, 2022 03:25
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
@stuhood stuhood disabled auto-merge August 18, 2022 21:48
@stuhood stuhood mentioned this pull request Aug 18, 2022
@stuhood
Copy link
Member

stuhood commented Aug 18, 2022

Ok: according to debugging over in #16578, the --no-colors flag is being respected in the three failing tests, but some additional un-stripped characters are still in the output, but only on Linux.

This actually smells a little bit like an issue that I saw in Byron/prodash#9 (comment): namely that to strip some of these characters, you actually need a full terminal emulator (oy). If there are no Python packages that do that, then adding a dependency on the strip_ansi_escapes crate and exposing a Python function for it might work here?

@Eric-Arellano
Copy link
Contributor

In the interest of landing this, we could split the PR in two also and only land the line wrapping for now?

huonw added 2 commits August 19, 2022 09:13
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
@huonw
Copy link
Contributor Author

huonw commented Aug 18, 2022

Ok: according to debugging over in #16578, the --no-colors flag is being respected in the three failing tests, but some additional un-stripped characters are still in the output, but only on Linux.

Ah, bummer, thanks for attempting it. I was hoping that downgrading to TERM=linux would be enough...

If there are no Python packages that do that

There's https://github.com/selectel/pyte but I have no experience with any options here, personally. I'd appreciate guidance about your preferred path forward since I am not.

In the interest of landing this, we could split the PR in two also and only land the line wrapping for now?

I've reverted all the colour parts of this, keeping only the line wrapping. I'll open a new PR with the rest in a little while.

@huonw huonw changed the title Show colors and disable line wrapping in mypy output Show disable line wrapping in mypy output Aug 18, 2022
@huonw huonw changed the title Show disable line wrapping in mypy output Disable 80 col hard wrapping in mypy output Aug 18, 2022
@Eric-Arellano Eric-Arellano enabled auto-merge (squash) August 19, 2022 00:19
@Eric-Arellano Eric-Arellano merged commit 7074e84 into pantsbuild:main Aug 19, 2022
@huonw huonw deleted the bugfix/16451-force-mypy branch August 19, 2022 04:20
@huonw
Copy link
Contributor Author

huonw commented Aug 19, 2022

I'll open a new PR with the rest in a little while.

#16586 (comment)

cczona pushed a commit to cczona/pants that referenced this pull request Sep 1, 2022
This fixes part of pantsbuild#16451 by ensuring that the mypy output isn't truncated/hard-wrapped at all. In particular: the terminal width is forcibly set to be very wide ("infinite") because soft-wrapped display in a wide terminal is nicer than it being hard-wrapped/truncated to 80 cols.

This seems to only be possible via an undocumented environment variable: `MYPY_FORCE_TERMINAL_WIDTH`: https://github.com/python/mypy/blob/03638dd670373db0b8f00cc3bcec256d09729d06/mypy/util.py#L468

[ci skip-rust]
stuhood pushed a commit that referenced this pull request Sep 9, 2022
This fixes #16451 by continuing the color half of #16488 (terminal width).

It does this by forcing mypy to always include color escapes, and then stripping them out in pants, based on the [GLOBAL].colors setting.

This seems to only be possible via an undocumented environment variable: `MYPY_FORCE_COLOR`: python/mypy#7771, https://github.com/python/mypy/blob/03638dd670373db0b8f00cc3bcec256d09729d06/mypy/util.py#L543

Doing this also requires setting `TERM=linux` so that curses can find appropriate info in the terminfo database.

[ci skip-rust]
[ci skip-build-wheels]
stuhood pushed a commit to stuhood/pants that referenced this pull request Sep 9, 2022
…build#16586)

This fixes pantsbuild#16451 by continuing the color half of pantsbuild#16488 (terminal width).

It does this by forcing mypy to always include color escapes, and then stripping them out in pants, based on the [GLOBAL].colors setting.

This seems to only be possible via an undocumented environment variable: `MYPY_FORCE_COLOR`: python/mypy#7771, https://github.com/python/mypy/blob/03638dd670373db0b8f00cc3bcec256d09729d06/mypy/util.py#L543

Doing this also requires setting `TERM=linux` so that curses can find appropriate info in the terminfo database.

[ci skip-rust]
[ci skip-build-wheels]
stuhood added a commit that referenced this pull request Sep 9, 2022
…y-pick of #16586) (#16808)

This fixes #16451 by continuing the color half of #16488 (terminal width).

It does this by forcing mypy to always include color escapes, and then stripping them out in pants, based on the [GLOBAL].colors setting.

This seems to only be possible via an undocumented environment variable: `MYPY_FORCE_COLOR`: python/mypy#7771, https://github.com/python/mypy/blob/03638dd670373db0b8f00cc3bcec256d09729d06/mypy/util.py#L543

Doing this also requires setting `TERM=linux` so that curses can find appropriate info in the terminfo database.

[ci skip-rust]
[ci skip-build-wheels]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants