Repository: pantsbuild/pants

Issue:
mypy output is limited to 80 characters wide and no colours
**Describe the bug**

Mypy has various nifty affordances to make it easier to understand its errors, e.g. colours and (with `--pretty`) printing the relevant line. It seems like pants currently gets in the way of these, presumably because mypy is detecting the non-tty output buffering.

For instance, https://gist.github.com/ce2e03d8788848ad6db27df6d66cde6d

```python
a_really_really_really_long_variable_name = 12345678901234567890 + "this makes the line really long"
```


Running mypy via `./pants check ::` gives output:

![image](https://user-images.githubusercontent.com/1203825/183544558-131305c6-5eee-4ae0-a49a-bb9af6e6832f.png)


While running it normally `mypy --pretty broken.py` (with the same version 0.961 installed via pip) gives output:

![image](https://user-images.githubusercontent.com/1203825/183544573-365dcce1-650d-4551-9f84-b47928cd4c75.png)

The first output is worse:

- line truncation despite (much) wider terminal (if the main error message was longer, it'd be wrapped, too, which makes it extra hard to read)
- lack of colours/bold

**Pants version**

- 2.12.0
- 2.14.0.dev5

**OS**
macOS

**Additional info**

Exact reproduction steps:

```shell
git clone https://gist.github.com/ce2e03d8788848ad6db27df6d66cde6d
cd ce2e03d8788848ad6db27df6d66cde6d

./pants check ::

mypy --version
mypy --pretty broken.py
```



Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/typecheck/mypy/rules.py b/src/python/pants/backend/python/typecheck/mypy/rules.py
--- a/src/python/pants/backend/python/typecheck/mypy/rules.py
+++ b/src/python/pants/backend/python/typecheck/mypy/rules.py
@@ -44,6 +44,7 @@
 from pants.engine.rules import Get, MultiGet, collect_rules, rule, rule_helper
 from pants.engine.target import CoarsenedTargets, FieldSet, Target
 from pants.engine.unions import UnionRule
+from pants.option.global_options import GlobalOptions
 from pants.util.logging import LogLevel
 from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
 from pants.util.strutil import pluralize, shell_quote
@@ -144,6 +145,7 @@ async def mypy_typecheck_partition(
     mkdir: MkdirBinary,
     cp: CpBinary,
     mv: MvBinary,
+    global_options: GlobalOptions,
 ) -> CheckResult:
     # MyPy requires 3.5+ to run, but uses the typed-ast library to work with 2.7, 3.4, 3.5, 3.6,
     # and 3.7. However, typed-ast does not understand 3.8+, so instead we must run MyPy with
@@ -319,6 +321,13 @@ async def mypy_typecheck_partition(
     env = {
         "PEX_EXTRA_SYS_PATH": ":".join(all_used_source_roots),
         "MYPYPATH": ":".join(all_used_source_roots),
+        # Always emit colors to improve cache hit rates, the results are post-processed to match the
+        # global setting
+        "MYPY_FORCE_COLOR": "1",
+        # Mypy needs to know the terminal so it can use appropriate escape sequences. ansi is a
+        # reasonable lowest common denominator for the sort of escapes mypy uses (NB. TERM=xterm
+        # uses some additional codes that colors.strip_color doesn't remove).
+        "TERM": "ansi",
         # Force a fixed terminal width. This is effectively infinite, disabling mypy's
         # builtin truncation and line wrapping. Terminals do an acceptable job of soft-wrapping
         # diagnostic text and source code is typically already hard-wrapped to a limited width.
@@ -342,7 +351,10 @@ async def mypy_typecheck_partition(
     result = await Get(FallibleProcessResult, Process, process)
     report = await Get(Digest, RemovePrefix(result.output_digest, REPORT_DIR))
     return CheckResult.from_fallible_process_result(
-        result, partition_description=partition.description(), report=report
+        result,
+        partition_description=partition.description(),
+        report=report,
+        strip_formatting=not global_options.colors,
     )
 
 
diff --git a/src/python/pants/core/goals/check.py b/src/python/pants/core/goals/check.py
--- a/src/python/pants/core/goals/check.py
+++ b/src/python/pants/core/goals/check.py
@@ -7,6 +7,8 @@
 from dataclasses import dataclass
 from typing import Any, Iterable, cast
 
+import colors
+
 from pants.core.goals.lint import REPORT_DIR as REPORT_DIR  # noqa: F401
 from pants.core.goals.style_request import (
     StyleRequest,
@@ -46,10 +48,13 @@ def from_fallible_process_result(
         *,
         partition_description: str | None = None,
         strip_chroot_path: bool = False,
+        strip_formatting: bool = False,
         report: Digest = EMPTY_DIGEST,
     ) -> CheckResult:
         def prep_output(s: bytes) -> str:
-            return strip_v2_chroot_path(s) if strip_chroot_path else s.decode()
+            chroot = strip_v2_chroot_path(s) if strip_chroot_path else s.decode()
+            formatting = cast(str, colors.strip_color(chroot)) if strip_formatting else chroot
+            return formatting
 
         return CheckResult(
             exit_code=process_result.exit_code,


Test patch:
diff --git a/src/python/pants/backend/python/typecheck/mypy/rules_integration_test.py b/src/python/pants/backend/python/typecheck/mypy/rules_integration_test.py
--- a/src/python/pants/backend/python/typecheck/mypy/rules_integration_test.py
+++ b/src/python/pants/backend/python/typecheck/mypy/rules_integration_test.py
@@ -900,4 +900,6 @@ class incredibly_long_type_name_to_force_wrapping_if_mypy_wrapped_error_messages
     assert re.search(
         "error:.*incredibly_long_type_name.*incredibly_long_attribute_name", result[0].stdout
     )
+    # at least one escape sequence that sets text color (red)
+    assert "\033[31m" in result[0].stdout
     assert result[0].report == EMPTY_DIGEST
diff --git a/src/python/pants/core/goals/check_test.py b/src/python/pants/core/goals/check_test.py
--- a/src/python/pants/core/goals/check_test.py
+++ b/src/python/pants/core/goals/check_test.py
@@ -8,6 +8,8 @@
 from textwrap import dedent
 from typing import Iterable, Optional, Sequence, Tuple, Type
 
+import pytest
+
 from pants.core.goals.check import (
     Check,
     CheckRequest,
@@ -18,7 +20,9 @@
 )
 from pants.core.util_rules.distdir import DistDir
 from pants.engine.addresses import Address
-from pants.engine.fs import Workspace
+from pants.engine.fs import EMPTY_DIGEST, EMPTY_FILE_DIGEST, Workspace
+from pants.engine.platform import Platform
+from pants.engine.process import FallibleProcessResult, ProcessResultMetadata
 from pants.engine.target import FieldSet, MultipleSourcesField, Target, Targets
 from pants.engine.unions import UnionMembership
 from pants.testutil.option_util import create_options_bootstrapper, create_subsystem
@@ -242,3 +246,34 @@ def test_streaming_output_partitions() -> None:
 
         """
     )
+
+
+@pytest.mark.parametrize(
+    ("strip_chroot_path", "strip_formatting", "expected"),
+    [
+        (False, False, "\033[0;31m/var/pants-sandbox-123/red/path.py\033[0m \033[1mbold\033[0m"),
+        (False, True, "/var/pants-sandbox-123/red/path.py bold"),
+        (True, False, "\033[0;31mred/path.py\033[0m \033[1mbold\033[0m"),
+        (True, True, "red/path.py bold"),
+    ],
+)
+def test_from_fallible_process_result_output_prepping(
+    strip_chroot_path: bool, strip_formatting: bool, expected: str
+) -> None:
+    result = CheckResult.from_fallible_process_result(
+        FallibleProcessResult(
+            exit_code=0,
+            stdout=b"stdout \033[0;31m/var/pants-sandbox-123/red/path.py\033[0m \033[1mbold\033[0m",
+            stdout_digest=EMPTY_FILE_DIGEST,
+            stderr=b"stderr \033[0;31m/var/pants-sandbox-123/red/path.py\033[0m \033[1mbold\033[0m",
+            stderr_digest=EMPTY_FILE_DIGEST,
+            output_digest=EMPTY_DIGEST,
+            platform=Platform.current,
+            metadata=ProcessResultMetadata(0, "ran_locally", 0),
+        ),
+        strip_chroot_path=strip_chroot_path,
+        strip_formatting=strip_formatting,
+    )
+
+    assert result.stdout == "stdout " + expected
+    assert result.stderr == "stderr " + expected