From 433159b00839b0b0f44ddbf0153bd76e788349e0 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:48:00 +0800 Subject: [PATCH 01/17] Some progress --- mypy/main.py | 49 +++++++++++++++++++++++++--------- mypy/options.py | 2 +- mypy/test/test_color_output.py | 27 +++++++++++++++++++ mypy/util.py | 46 ++++++++++++++++++------------- 4 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 mypy/test/test_color_output.py diff --git a/mypy/main.py b/mypy/main.py index fd50c7677a11..39b45e410e17 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -124,7 +124,10 @@ def main( install_types(formatter, options, non_interactive=options.non_interactive) return - res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr) + use_color = (formatter.default_colored if options.color_output == "auto" + else bool(options.color_output)) + + res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) if options.non_interactive: missing_pkgs = read_types_packages_to_install(options.cache_dir, after_run=True) @@ -134,7 +137,7 @@ def main( fscache.flush() print() res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr) - show_messages(messages, stderr, formatter, options) + show_messages(messages, stderr, formatter, options, use_color) if MEM_PROFILE: from mypy.memprofile import print_memory_profile @@ -148,12 +151,12 @@ def main( if options.error_summary: if n_errors: summary = formatter.format_error( - n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output + n_errors, n_files, len(sources), blockers=blockers, use_color=use_color ) stdout.write(summary + "\n") # Only notes should also output success elif not messages or n_notes == len(messages): - stdout.write(formatter.format_success(len(sources), options.color_output) + "\n") + stdout.write(formatter.format_success(len(sources), use_color) + "\n") stdout.flush() if options.install_types and not options.non_interactive: @@ -182,6 +185,7 @@ def run_build( t0: float, stdout: TextIO, stderr: TextIO, + use_color: bool, ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) @@ -200,7 +204,7 @@ def flush_errors(filename: str | None, new_messages: list[str], serious: bool) - # Collect messages and possibly show them later. return f = stderr if serious else stdout - show_messages(new_messages, f, formatter, options) + show_messages(new_messages, f, formatter, options, use_color) serious = False blockers = False @@ -238,10 +242,11 @@ def flush_errors(filename: str | None, new_messages: list[str], serious: bool) - def show_messages( - messages: list[str], f: TextIO, formatter: util.FancyFormatter, options: Options + messages: list[str], f: TextIO, formatter: util.FancyFormatter, options: Options, + use_color: bool ) -> None: for msg in messages: - if options.color_output: + if use_color: msg = formatter.colorize(msg) f.write(msg + "\n") f.flush() @@ -461,6 +466,13 @@ def __call__( parser._print_message(formatter.format_help(), self.stdout) parser.exit() +# Coupled with the usage in define_options +class ColorOutputAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + assert values in ("auto", None) + print(f"{values=}") + setattr(namespace, self.dest, True if values is None else "auto") + def define_options( program: str = "mypy", @@ -993,13 +1005,25 @@ def add_invertible_flag( " and show error location markers", group=error_group, ) - add_invertible_flag( + # XXX Setting default doesn't seem to work unless I change + # the attribute in options.Options + error_group.add_argument( + "--color-output", + dest="color_output", + action=ColorOutputAction, + nargs="?", + choices=["auto"], + help="Colorize error messages (inverse: --no-color-output). " + "Detects if to use color when option is omitted and --no-color-output " + "is not given, or when --color-output=auto", + ) + error_group.add_argument( "--no-color-output", dest="color_output", - default=True, - help="Do not colorize error messages", - group=error_group, + action="store_false", + help=argparse.SUPPRESS, ) + #error_group.set_defaults(color_output="auto") add_invertible_flag( "--no-error-summary", dest="error_summary", @@ -1527,7 +1551,8 @@ def set_strict_flags() -> None: reason = cache.find_module(p) if reason is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: fail( - f"Package '{p}' cannot be type checked due to missing py.typed marker. See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details", + f"Package '{p}' cannot be type checked due to missing py.typed marker. " + "See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details", stderr, options, ) diff --git a/mypy/options.py b/mypy/options.py index 6d7eca772888..791805aeea7b 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -205,7 +205,7 @@ def __init__(self) -> None: self.show_error_context = False # Use nicer output (when possible). - self.color_output = True + self.color_output = "auto" self.error_summary = True # Assume arguments with default values of None are Optional diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py new file mode 100644 index 000000000000..3a7cf8ba1e48 --- /dev/null +++ b/mypy/test/test_color_output.py @@ -0,0 +1,27 @@ +from subprocess import run, PIPE +from functools import partial +from typing import Any + +# TODO Would like help with this test, how do I make it runnable? + +def test_color_output() -> None: + # Note: Though we don't check stderr, capturing it is useful + # because it provides traceback if mypy crashes due to exception + # and pytest reveals it upon failure (?) + def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: + res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) + if expect_color: # Expect color control chars + assert ":1: error:" not in res.stdout + assert "]\nFound" not in res.stdout + else: # Expect no color control chars + assert ":1: error:" in res.stdout + assert "]\nFound" in res.stdout + colored = partial(test, True) + not_colored = partial(test, False) + not_colored("mypy -c \"1+'a'\"") + colored("mypy -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"}) + colored("mypy -c \"1+'a'\" --color-output") + not_colored("mypy -c \"1+'a'\" --no-color-output") + colored("mypy -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) + +# TODO: Tests in the terminal (require manual testing?) \ No newline at end of file diff --git a/mypy/util.py b/mypy/util.py index d7ff2a367fa2..d852b11dbc41 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -579,10 +579,11 @@ def parse_gray_color(cup: bytes) -> str: def should_force_color() -> bool: env_var = os.getenv("MYPY_FORCE_COLOR", os.getenv("FORCE_COLOR", "0")) + # This logic feels a bit counter-intuitive but it's legacy so rather preserve it try: - return bool(int(env_var)) + return int(env_var) != 0 except ValueError: - return bool(env_var) + return env_var != "" class FancyFormatter: @@ -596,21 +597,9 @@ def __init__( ) -> 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 - return - if not should_force_color() and (not f_out.isatty() or not f_err.isatty()): - self.dummy_term = True - return - if sys.platform == "win32": - self.dummy_term = not self.initialize_win_colors() - elif sys.platform == "emscripten": - self.dummy_term = not self.initialize_vt100_colors() - else: - self.dummy_term = not self.initialize_unix_colors() - if not self.dummy_term: + self.default_colored = f_out.isatty() and f_err.isatty() + self.colors_ok = self.detect_terminal_colors(f_out, f_err) + if self.colors_ok: self.colors = { "red": self.RED, "green": self.GREEN, @@ -618,6 +607,27 @@ def __init__( "yellow": self.YELLOW, "none": "", } + else: + if should_force_color(): + print("warning: failed to detect a suitable color scheme " + "but MYPY_FORCE_COLOR or FORCE_COLOR is enabled") + self.colors = { + "red": "", + "green": "", + "blue": "", + "yellow": "", + "none": "", + } + + def detect_terminal_colors(self, f_out: IO[str], f_err: IO[str]) -> bool: + if sys.platform not in ("linux", "darwin", "win32", "emscripten"): + return False + if sys.platform == "win32": + return self.initialize_win_colors() + elif sys.platform == "emscripten": + return self.initialize_vt100_colors() + else: + return self.initialize_unix_colors() def initialize_vt100_colors(self) -> bool: """Return True if initialization was successful and we can use colors, False otherwise""" @@ -709,8 +719,6 @@ def style( dim: bool = False, ) -> str: """Apply simple color and style (underlined or bold).""" - if self.dummy_term: - return text if bold: start = self.BOLD else: From 807f022c0cc6adbf2e976f0f19881ce1c6cb4a7b Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:59:45 +0800 Subject: [PATCH 02/17] More changes --- mypy/main.py | 3 ++- mypy/test/test_color_output.py | 45 +++++++++++++++++++++++++--------- mypy/util.py | 4 +-- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 39b45e410e17..3b98f52f8978 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -124,7 +124,8 @@ def main( install_types(formatter, options, non_interactive=options.non_interactive) return - use_color = (formatter.default_colored if options.color_output == "auto" + use_color = (True if util.should_force_color() + else formatter.default_colored if options.color_output == "auto" else bool(options.color_output)) res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 3a7cf8ba1e48..9a4af8a2d48f 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -4,24 +4,45 @@ # TODO Would like help with this test, how do I make it runnable? +def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: + res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) + if expect_color: # Expect color control chars + assert ":1: error:" not in res.stdout + assert "\nFound" not in res.stdout + else: # Expect no color control chars + assert ":1: error:" in res.stdout + assert "\nFound" in res.stdout + +colored = partial(test, True) +not_colored = partial(test, False) + def test_color_output() -> None: # Note: Though we don't check stderr, capturing it is useful # because it provides traceback if mypy crashes due to exception # and pytest reveals it upon failure (?) - def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: - res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) - if expect_color: # Expect color control chars - assert ":1: error:" not in res.stdout - assert "]\nFound" not in res.stdout - else: # Expect no color control chars - assert ":1: error:" in res.stdout - assert "]\nFound" in res.stdout - colored = partial(test, True) - not_colored = partial(test, False) not_colored("mypy -c \"1+'a'\"") colored("mypy -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"}) colored("mypy -c \"1+'a'\" --color-output") not_colored("mypy -c \"1+'a'\" --no-color-output") - colored("mypy -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) + colored("mypy -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) #TODO + +# TODO: Tests in the terminal (require manual testing?) +""" +In the terminal: + colored: mypy -c "1+'a'" + colored: mypy -c "1+'a'" --color-output +not colored: mypy -c "1+'a'" --no-color-output + colored: mypy -c "1+'a'" --color-output (with MYPY_FORCE_COLOR=1) + colored: mypy -c "1+'a'" --no-color-output (with MYPY_FORCE_COLOR=1) + +To test, save this as a .bat and run in a Windows terminal (I don't know the Unix equivalent): -# TODO: Tests in the terminal (require manual testing?) \ No newline at end of file +set MYPY_FORCE_COLOR= +mypy -c "1+'a'" +mypy -c "1+'a'" --color-output +mypy -c "1+'a'" --no-color-output +set MYPY_FORCE_COLOR=1 +mypy -c "1+'a'" --color-output +mypy -c "1+'a'" --no-color-output +set MYPY_FORCE_COLOR= +""" \ No newline at end of file diff --git a/mypy/util.py b/mypy/util.py index d852b11dbc41..4e2c20a8ec80 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -609,8 +609,8 @@ def __init__( } else: if should_force_color(): - print("warning: failed to detect a suitable color scheme " - "but MYPY_FORCE_COLOR or FORCE_COLOR is enabled") + print("warning: failed to detect a suitable terminal color scheme " + "but MYPY_FORCE_COLOR or FORCE_COLOR is set to on") self.colors = { "red": "", "green": "", From 30e8e43d36dc6dc7424df06b550b2f8914cd7d29 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:47:14 +0800 Subject: [PATCH 03/17] Document the option --- docs/source/command_line.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index db2407e17df8..df13280a15e7 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -923,9 +923,27 @@ in error messages. Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location markers. +.. option:: --color-output[=auto] + + Enables colored output in error messages. + + When ``--color-output=auto`` is given, uses colored output if the output + (both stdout and stderr) is going to a tty. This is also the default. + + .. note:: + When the environment variable ``MYPY_FORCE_COLOR`` is set to a + non-``0`` non-empty string, mypy always enables colored output + (even if ``--no-color-output`` is given). + + .. Note: Here I decide not to document ``FORCE_COLOR`` as its + logic seems counter-intuitive from earlier conventions + (PR13853) + .. option:: --no-color-output - This flag will disable color output in error messages, enabled by default. + Disables colored output in error messages. + + See also note above. .. option:: --no-error-summary From ff79d3ca92b96d7df0219d8d159c369183f1af7c Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:03:52 +0800 Subject: [PATCH 04/17] Fix type errors --- mypy/dmypy_server.py | 5 ++++- mypy/main.py | 5 +++-- mypy/test/test_color_output.py | 18 +++++++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 33e9e07477ca..c270f59ae288 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -21,6 +21,7 @@ from typing import Any, Callable, Final from typing_extensions import TypeAlias as _TypeAlias +from mypy import util import mypy.build import mypy.errors import mypy.main @@ -841,7 +842,9 @@ def pretty_messages( is_tty: bool = False, terminal_width: int | None = None, ) -> list[str]: - use_color = self.options.color_output and is_tty + use_color = (True if util.should_force_color() + else self.formatter.default_colored if self.options.color_output == "auto" + else bool(self.options.color_output)) fit_width = self.options.pretty and is_tty if fit_width: messages = self.formatter.fit_in_terminal( diff --git a/mypy/main.py b/mypy/main.py index 3b98f52f8978..15c89d784257 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -137,7 +137,7 @@ def main( install_types(formatter, options, after_run=True, non_interactive=True) fscache.flush() print() - res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr) + res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) show_messages(messages, stderr, formatter, options, use_color) if MEM_PROFILE: @@ -469,7 +469,8 @@ def __call__( # Coupled with the usage in define_options class ColorOutputAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + values: str | Sequence[Any] | None, option_string: str | None = None) -> None: assert values in ("auto", None) print(f"{values=}") setattr(namespace, self.dest, True if values is None else "auto") diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 9a4af8a2d48f..58998963fd81 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -1,11 +1,14 @@ from subprocess import run, PIPE from functools import partial from typing import Any +import pytest # TODO Would like help with this test, how do I make it runnable? def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: - res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) + res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) #type:ignore[call-overload] + if "Found" not in res.stdout: #?? + pytest.fail("Command failed to complete or did not detect type error") if expect_color: # Expect color control chars assert ":1: error:" not in res.stdout assert "\nFound" not in res.stdout @@ -16,15 +19,16 @@ def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: colored = partial(test, True) not_colored = partial(test, False) -def test_color_output() -> None: +@pytest.mark.parametrize("command", ["mypy", "dmypy run --"]) +def test_color_output(command: str) -> None: # Note: Though we don't check stderr, capturing it is useful # because it provides traceback if mypy crashes due to exception # and pytest reveals it upon failure (?) - not_colored("mypy -c \"1+'a'\"") - colored("mypy -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"}) - colored("mypy -c \"1+'a'\" --color-output") - not_colored("mypy -c \"1+'a'\" --no-color-output") - colored("mypy -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) #TODO + not_colored(f"{command} -c \"1+'a'\"") + colored(f"{command} -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"}) + colored(f"{command} -c \"1+'a'\" --color-output") + not_colored(f"{command} -c \"1+'a'\" --no-color-output") + colored(f"{command} -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) #TODO # TODO: Tests in the terminal (require manual testing?) """ From b3c4e871d790d8ed08639a8aefce178277aad99b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:31:48 +0000 Subject: [PATCH 05/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/dmypy_server.py | 14 +++++++---- mypy/main.py | 44 ++++++++++++++++++++++------------ mypy/test/test_color_output.py | 19 +++++++++------ mypy/util.py | 14 ++++------- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index c270f59ae288..f2c27e529aa8 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -21,10 +21,10 @@ from typing import Any, Callable, Final from typing_extensions import TypeAlias as _TypeAlias -from mypy import util import mypy.build import mypy.errors import mypy.main +from mypy import util from mypy.dmypy_util import WriteToConn, receive, send from mypy.find_sources import InvalidSourceList, create_source_list from mypy.fscache import FileSystemCache @@ -842,9 +842,15 @@ def pretty_messages( is_tty: bool = False, terminal_width: int | None = None, ) -> list[str]: - use_color = (True if util.should_force_color() - else self.formatter.default_colored if self.options.color_output == "auto" - else bool(self.options.color_output)) + use_color = ( + True + if util.should_force_color() + else ( + self.formatter.default_colored + if self.options.color_output == "auto" + else bool(self.options.color_output) + ) + ) fit_width = self.options.pretty and is_tty if fit_width: messages = self.formatter.fit_in_terminal( diff --git a/mypy/main.py b/mypy/main.py index 15c89d784257..816f683da1bc 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -124,9 +124,15 @@ def main( install_types(formatter, options, non_interactive=options.non_interactive) return - use_color = (True if util.should_force_color() - else formatter.default_colored if options.color_output == "auto" - else bool(options.color_output)) + use_color = ( + True + if util.should_force_color() + else ( + formatter.default_colored + if options.color_output == "auto" + else bool(options.color_output) + ) + ) res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) @@ -137,7 +143,9 @@ def main( install_types(formatter, options, after_run=True, non_interactive=True) fscache.flush() print() - res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) + res, messages, blockers = run_build( + sources, options, fscache, t0, stdout, stderr, use_color + ) show_messages(messages, stderr, formatter, options, use_color) if MEM_PROFILE: @@ -243,8 +251,11 @@ def flush_errors(filename: str | None, new_messages: list[str], serious: bool) - def show_messages( - messages: list[str], f: TextIO, formatter: util.FancyFormatter, options: Options, - use_color: bool + messages: list[str], + f: TextIO, + formatter: util.FancyFormatter, + options: Options, + use_color: bool, ) -> None: for msg in messages: if use_color: @@ -467,10 +478,16 @@ def __call__( parser._print_message(formatter.format_help(), self.stdout) parser.exit() + # Coupled with the usage in define_options class ColorOutputAction(argparse.Action): - def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: str | Sequence[Any] | None, option_string: str | None = None) -> None: + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[Any] | None, + option_string: str | None = None, + ) -> None: assert values in ("auto", None) print(f"{values=}") setattr(namespace, self.dest, True if values is None else "auto") @@ -1016,16 +1033,13 @@ def add_invertible_flag( nargs="?", choices=["auto"], help="Colorize error messages (inverse: --no-color-output). " - "Detects if to use color when option is omitted and --no-color-output " - "is not given, or when --color-output=auto", + "Detects if to use color when option is omitted and --no-color-output " + "is not given, or when --color-output=auto", ) error_group.add_argument( - "--no-color-output", - dest="color_output", - action="store_false", - help=argparse.SUPPRESS, + "--no-color-output", dest="color_output", action="store_false", help=argparse.SUPPRESS ) - #error_group.set_defaults(color_output="auto") + # error_group.set_defaults(color_output="auto") add_invertible_flag( "--no-error-summary", dest="error_summary", diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 58998963fd81..f34dacbbc53c 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -1,24 +1,28 @@ -from subprocess import run, PIPE from functools import partial +from subprocess import PIPE, run from typing import Any + import pytest # TODO Would like help with this test, how do I make it runnable? + def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: - res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) #type:ignore[call-overload] - if "Found" not in res.stdout: #?? + res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) # type:ignore[call-overload] + if "Found" not in res.stdout: # ?? pytest.fail("Command failed to complete or did not detect type error") - if expect_color: # Expect color control chars + if expect_color: # Expect color control chars assert ":1: error:" not in res.stdout assert "\nFound" not in res.stdout - else: # Expect no color control chars + else: # Expect no color control chars assert ":1: error:" in res.stdout assert "\nFound" in res.stdout + colored = partial(test, True) not_colored = partial(test, False) + @pytest.mark.parametrize("command", ["mypy", "dmypy run --"]) def test_color_output(command: str) -> None: # Note: Though we don't check stderr, capturing it is useful @@ -28,7 +32,8 @@ def test_color_output(command: str) -> None: colored(f"{command} -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"}) colored(f"{command} -c \"1+'a'\" --color-output") not_colored(f"{command} -c \"1+'a'\" --no-color-output") - colored(f"{command} -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) #TODO + colored(f"{command} -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) # TODO + # TODO: Tests in the terminal (require manual testing?) """ @@ -49,4 +54,4 @@ def test_color_output(command: str) -> None: mypy -c "1+'a'" --color-output mypy -c "1+'a'" --no-color-output set MYPY_FORCE_COLOR= -""" \ No newline at end of file +""" diff --git a/mypy/util.py b/mypy/util.py index 4e2c20a8ec80..e56b381a5570 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -609,15 +609,11 @@ def __init__( } else: if should_force_color(): - print("warning: failed to detect a suitable terminal color scheme " - "but MYPY_FORCE_COLOR or FORCE_COLOR is set to on") - self.colors = { - "red": "", - "green": "", - "blue": "", - "yellow": "", - "none": "", - } + print( + "warning: failed to detect a suitable terminal color scheme " + "but MYPY_FORCE_COLOR or FORCE_COLOR is set to on" + ) + self.colors = {"red": "", "green": "", "blue": "", "yellow": "", "none": ""} def detect_terminal_colors(self, f_out: IO[str], f_err: IO[str]) -> bool: if sys.platform not in ("linux", "darwin", "win32", "emscripten"): From 068f116c4b194477ed4f786fbd51228e51927b99 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:42:27 +0800 Subject: [PATCH 06/17] Fix Ruff lint UP022 --- mypy/test/test_color_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 58998963fd81..1f2b868d5e33 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -6,7 +6,7 @@ # TODO Would like help with this test, how do I make it runnable? def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: - res = run(*args, stdout=PIPE, stderr=PIPE, **kwargs) #type:ignore[call-overload] + res = run(*args, capture_output=True, **kwargs) #type:ignore[call-overload] if "Found" not in res.stdout: #?? pytest.fail("Command failed to complete or did not detect type error") if expect_color: # Expect color control chars From cc905b82783ca824bc371a9df2a26722b3c7a348 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:44:55 +0000 Subject: [PATCH 07/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/test/test_color_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 0b0f0accbc86..eede60b24945 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -1,5 +1,5 @@ from functools import partial -from subprocess import PIPE, run +from subprocess import run from typing import Any import pytest From 2dfb813d6d268090c9202c2f4488154096c9a7af Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:47:58 +0800 Subject: [PATCH 08/17] lint: Delete unused "type: ignore" comment --- mypy/test/test_color_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/test_color_output.py b/mypy/test/test_color_output.py index 0b0f0accbc86..3c79f43b88e3 100644 --- a/mypy/test/test_color_output.py +++ b/mypy/test/test_color_output.py @@ -8,7 +8,7 @@ def test(expect_color: bool, *args: Any, **kwargs: Any) -> None: - res = run(*args, capture_output=True, **kwargs) # type:ignore[call-overload] + res = run(*args, capture_output=True, **kwargs) if "Found" not in res.stdout: # ?? pytest.fail("Command failed to complete or did not detect type error") if expect_color: # Expect color control chars From 819b8676edb41e88d0adc0bb85cba9b27dc41dd4 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:39:11 +0800 Subject: [PATCH 09/17] Some fixes --- mypy/dmypy_server.py | 16 ++++++++++++++-- mypy/main.py | 35 +++++++++++++++++++---------------- mypy/util.py | 26 +++++++++++++++++--------- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index f2c27e529aa8..19da4f1cbabb 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -18,7 +18,7 @@ import traceback from collections.abc import Sequence, Set as AbstractSet from contextlib import redirect_stderr, redirect_stdout -from typing import Any, Callable, Final +from typing import Any, Callable, Final, Literal, cast from typing_extensions import TypeAlias as _TypeAlias import mypy.build @@ -199,10 +199,22 @@ def __init__(self, options: Options, status_file: str, timeout: int | None = Non # (details in https://github.com/python/mypy/issues/4492) options.local_partial_types = True self.status_file = status_file + + # Type annotation needed for mypy (Pyright understands this) + use_color: bool | Literal["auto"] = ( + True + if util.should_force_color() + else ( + "auto" + if options.color_output == "auto" + else cast(bool, options.color_output) + ) + ) # Since the object is created in the parent process we can check # the output terminal options here. - self.formatter = FancyFormatter(sys.stdout, sys.stderr, options.hide_error_codes) + self.formatter = FancyFormatter(sys.stdout, sys.stderr, options.hide_error_codes, + color_request=use_color) def _response_metadata(self) -> dict[str, str]: py_version = f"{self.options.python_version[0]}_{self.options.python_version[1]}" diff --git a/mypy/main.py b/mypy/main.py index 816f683da1bc..45a611f10332 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -11,7 +11,7 @@ from collections.abc import Sequence from gettext import gettext from io import TextIOWrapper -from typing import IO, TYPE_CHECKING, Any, Final, NoReturn, TextIO +from typing import IO, TYPE_CHECKING, Any, Final, Literal, NoReturn, TextIO, Union, cast from mypy import build, defaults, state, util from mypy.config_parser import ( @@ -90,8 +90,19 @@ def main( if clean_exit: options.fast_exit = False + # Type annotation needed for mypy (Pyright understands this) + use_color: bool | Literal["auto"] = ( + True + if util.should_force_color() + else ( + "auto" + if options.color_output == "auto" + else cast(bool, options.color_output) + ) + ) + formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) + stdout, stderr, options.hide_error_codes, hide_success=bool(options.output), color_request=use_color ) if options.allow_redefinition_new and not options.local_partial_types: @@ -124,16 +135,6 @@ def main( install_types(formatter, options, non_interactive=options.non_interactive) return - use_color = ( - True - if util.should_force_color() - else ( - formatter.default_colored - if options.color_output == "auto" - else bool(options.color_output) - ) - ) - res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color) if options.non_interactive: @@ -194,10 +195,11 @@ def run_build( t0: float, stdout: TextIO, stderr: TextIO, - use_color: bool, + use_color: bool | Literal["auto"], ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) + stdout, stderr, options.hide_error_codes, hide_success=bool(options.output), + color_request=use_color ) messages = [] @@ -255,8 +257,10 @@ def show_messages( f: TextIO, formatter: util.FancyFormatter, options: Options, - use_color: bool, + use_color: bool | Literal["auto"], ) -> None: + if use_color == "auto": + use_color = formatter.default_colored for msg in messages: if use_color: msg = formatter.colorize(msg) @@ -489,7 +493,6 @@ def __call__( option_string: str | None = None, ) -> None: assert values in ("auto", None) - print(f"{values=}") setattr(namespace, self.dest, True if values is None else "auto") diff --git a/mypy/util.py b/mypy/util.py index e56b381a5570..815cf0cfa406 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -12,7 +12,7 @@ import time from collections.abc import Container, Iterable, Sequence, Sized from importlib import resources as importlib_resources -from typing import IO, Any, Callable, Final, Literal, TypeVar +from typing import IO, TYPE_CHECKING, Any, Callable, Final, Literal, TypeVar, cast orjson: Any try: @@ -593,14 +593,19 @@ class FancyFormatter: """ def __init__( - self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool, hide_success: bool = False + self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool, hide_success: bool = False, + color_request: bool | Literal["auto"] = "auto" ) -> None: self.hide_error_codes = hide_error_codes self.hide_success = hide_success self.default_colored = f_out.isatty() and f_err.isatty() self.colors_ok = self.detect_terminal_colors(f_out, f_err) + self.RED: str + self.GREEN: str + self.BLUE: str + self.YELLOW: str if self.colors_ok: - self.colors = { + self.colors: dict[str, str] = { "red": self.RED, "green": self.GREEN, "blue": self.BLUE, @@ -608,12 +613,13 @@ def __init__( "none": "", } else: - if should_force_color(): + if color_request is True or (color_request == "auto" and self.default_colored): print( "warning: failed to detect a suitable terminal color scheme " - "but MYPY_FORCE_COLOR or FORCE_COLOR is set to on" + "but colored output is requested" ) self.colors = {"red": "", "green": "", "blue": "", "yellow": "", "none": ""} + self.NORMAL = self.BOLD = self.UNDER = self.DIM = "" def detect_terminal_colors(self, f_out: IO[str], f_err: IO[str]) -> bool: if sys.platform not in ("linux", "darwin", "win32", "emscripten"): @@ -723,6 +729,8 @@ def style( start += self.UNDER if dim: start += self.DIM + #if TYPE_CHECKING: + # reveal_type(self.colors) return start + self.colors[color] + text + self.NORMAL def fit_in_terminal( @@ -823,7 +831,7 @@ def underline_link(self, note: str) -> str: end = match.end() return note[:start] + self.style(note[start:end], "none", underline=True) + note[end:] - def format_success(self, n_sources: int, use_color: bool = True) -> str: + def format_success(self, n_sources: int, use_color: bool | Literal["auto"] = True) -> str: """Format short summary in case of success. n_sources is total number of files passed directly on command line, @@ -833,7 +841,7 @@ def format_success(self, n_sources: int, use_color: bool = True) -> str: return "" msg = f"Success: no issues found in {n_sources} source file{plural_s(n_sources)}" - if not use_color: + if not (use_color is True or (use_color == "auto" and self.default_colored)): return msg return self.style(msg, "green", bold=True) @@ -844,7 +852,7 @@ def format_error( n_sources: int, *, blockers: bool = False, - use_color: bool = True, + use_color: bool | Literal["auto"] = True, ) -> str: """Format a short summary in case of errors.""" msg = f"Found {n_errors} error{plural_s(n_errors)} in {n_files} file{plural_s(n_files)}" @@ -852,7 +860,7 @@ def format_error( msg += " (errors prevented further checking)" else: msg += f" (checked {n_sources} source file{plural_s(n_sources)})" - if not use_color: + if not (use_color is True or (use_color == "auto" and self.default_colored)): return msg return self.style(msg, "red", bold=True) From 5879267822a6a9141e5201cd1e2261387556fb07 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 02:40:37 +0000 Subject: [PATCH 10/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/dmypy_server.py | 13 +++++-------- mypy/main.py | 21 ++++++++++++--------- mypy/util.py | 12 ++++++++---- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 19da4f1cbabb..0074a9b16f25 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -199,22 +199,19 @@ def __init__(self, options: Options, status_file: str, timeout: int | None = Non # (details in https://github.com/python/mypy/issues/4492) options.local_partial_types = True self.status_file = status_file - + # Type annotation needed for mypy (Pyright understands this) use_color: bool | Literal["auto"] = ( True if util.should_force_color() - else ( - "auto" - if options.color_output == "auto" - else cast(bool, options.color_output) - ) + else ("auto" if options.color_output == "auto" else cast(bool, options.color_output)) ) # Since the object is created in the parent process we can check # the output terminal options here. - self.formatter = FancyFormatter(sys.stdout, sys.stderr, options.hide_error_codes, - color_request=use_color) + self.formatter = FancyFormatter( + sys.stdout, sys.stderr, options.hide_error_codes, color_request=use_color + ) def _response_metadata(self) -> dict[str, str]: py_version = f"{self.options.python_version[0]}_{self.options.python_version[1]}" diff --git a/mypy/main.py b/mypy/main.py index 45a611f10332..3bdfacbae509 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -11,7 +11,7 @@ from collections.abc import Sequence from gettext import gettext from io import TextIOWrapper -from typing import IO, TYPE_CHECKING, Any, Final, Literal, NoReturn, TextIO, Union, cast +from typing import IO, TYPE_CHECKING, Any, Final, Literal, NoReturn, TextIO, cast from mypy import build, defaults, state, util from mypy.config_parser import ( @@ -94,15 +94,15 @@ def main( use_color: bool | Literal["auto"] = ( True if util.should_force_color() - else ( - "auto" - if options.color_output == "auto" - else cast(bool, options.color_output) - ) + else ("auto" if options.color_output == "auto" else cast(bool, options.color_output)) ) formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=bool(options.output), color_request=use_color + stdout, + stderr, + options.hide_error_codes, + hide_success=bool(options.output), + color_request=use_color, ) if options.allow_redefinition_new and not options.local_partial_types: @@ -198,8 +198,11 @@ def run_build( use_color: bool | Literal["auto"], ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( - stdout, stderr, options.hide_error_codes, hide_success=bool(options.output), - color_request=use_color + stdout, + stderr, + options.hide_error_codes, + hide_success=bool(options.output), + color_request=use_color, ) messages = [] diff --git a/mypy/util.py b/mypy/util.py index 815cf0cfa406..d58e96cdda09 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -12,7 +12,7 @@ import time from collections.abc import Container, Iterable, Sequence, Sized from importlib import resources as importlib_resources -from typing import IO, TYPE_CHECKING, Any, Callable, Final, Literal, TypeVar, cast +from typing import IO, Any, Callable, Final, Literal, TypeVar orjson: Any try: @@ -593,8 +593,12 @@ class FancyFormatter: """ def __init__( - self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool, hide_success: bool = False, - color_request: bool | Literal["auto"] = "auto" + self, + f_out: IO[str], + f_err: IO[str], + hide_error_codes: bool, + hide_success: bool = False, + color_request: bool | Literal["auto"] = "auto", ) -> None: self.hide_error_codes = hide_error_codes self.hide_success = hide_success @@ -729,7 +733,7 @@ def style( start += self.UNDER if dim: start += self.DIM - #if TYPE_CHECKING: + # if TYPE_CHECKING: # reveal_type(self.colors) return start + self.colors[color] + text + self.NORMAL From bd4460da40ad3ad2e79baa8d3b8374212a49ca54 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:59:19 +0800 Subject: [PATCH 11/17] Remove MYPY_FORCE_COLOR and FORCE_COLOR in workflows/test.yml (??) FORCE_COLOR may impact other tools but I'll give it a try anyway ... --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47f725170bd8..362c61c58a99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,14 +111,14 @@ jobs: env: TOX_SKIP_MISSING_INTERPRETERS: False # Rich (pip) -- Disable color for windows + pytest - FORCE_COLOR: ${{ !(startsWith(matrix.os, 'windows-') && startsWith(matrix.toxenv, 'py')) && 1 || 0 }} + #FORCE_COLOR: ${{ !(startsWith(matrix.os, 'windows-') && startsWith(matrix.toxenv, 'py')) && 1 || 0 }} # Tox PY_COLORS: 1 # Python -- Disable argparse help colors (3.14+) PYTHON_COLORS: 0 # Mypy (see https://github.com/python/mypy/issues/7771) TERM: xterm-color - MYPY_FORCE_COLOR: 1 + #MYPY_FORCE_COLOR: 1 MYPY_FORCE_TERMINAL_WIDTH: 200 # Pytest PYTEST_ADDOPTS: --color=yes From 91be93d6a6c3c74ca8ab7d1d32e87d182ea5685e Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:14:00 +0800 Subject: [PATCH 12/17] Delete some workflows for this fork only (?) --- .github/workflows/build_wheels.yml | 25 --------------- .github/workflows/docs.yml | 48 ----------------------------- .github/workflows/sync_typeshed.yml | 36 ---------------------- .github/workflows/test_stubgenc.yml | 41 ------------------------ 4 files changed, 150 deletions(-) delete mode 100644 .github/workflows/build_wheels.yml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/sync_typeshed.yml delete mode 100644 .github/workflows/test_stubgenc.yml diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml deleted file mode 100644 index dae4937d5081..000000000000 --- a/.github/workflows/build_wheels.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Trigger wheel build - -on: - push: - branches: [main, master, 'release*'] - tags: ['*'] - -permissions: - contents: read - -jobs: - build-wheels: - if: github.repository == 'python/mypy' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Trigger script - env: - WHEELS_PUSH_TOKEN: ${{ secrets.WHEELS_PUSH_TOKEN }} - run: ./misc/trigger_wheel_build.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 3e78bf51913e..000000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Check documentation build - -on: - workflow_dispatch: - push: - branches: [main, master, 'release*'] - tags: ['*'] - pull_request: - paths: - - 'docs/**' - # We now have a docs check that fails if any error codes don't have documentation, - # so it's important to do the docs build on all PRs touching mypy/errorcodes.py - # in case somebody's adding a new error code without any docs - - 'mypy/errorcodes.py' - - 'mypyc/doc/**' - - '**/*.rst' - - '**/*.md' - - CREDITS - - LICENSE - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - docs: - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - TOXENV: docs - TOX_SKIP_MISSING_INTERPRETERS: False - VERIFY_MYPY_ERROR_CODES: 1 - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install tox - run: pip install tox==4.26.0 - - name: Setup tox environment - run: tox run -e ${{ env.TOXENV }} --notest - - name: Test - run: tox run -e ${{ env.TOXENV }} --skip-pkg-install diff --git a/.github/workflows/sync_typeshed.yml b/.github/workflows/sync_typeshed.yml deleted file mode 100644 index 2d5361a5919c..000000000000 --- a/.github/workflows/sync_typeshed.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Sync typeshed - -on: - workflow_dispatch: - schedule: - - cron: "0 0 1,15 * *" - -permissions: {} - -jobs: - sync_typeshed: - name: Sync typeshed - if: github.repository == 'python/mypy' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true # needed to `git push` the PR branch - # TODO: use whatever solution ends up working for - # https://github.com/python/typeshed/issues/8434 - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: git config - run: | - git config --global user.name mypybot - git config --global user.email '<>' - - name: Sync typeshed - run: | - python -m pip install requests==2.28.1 - GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} python misc/sync-typeshed.py --make-pr diff --git a/.github/workflows/test_stubgenc.yml b/.github/workflows/test_stubgenc.yml deleted file mode 100644 index 4676acf8695b..000000000000 --- a/.github/workflows/test_stubgenc.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Test stubgenc on pybind11_fixtures - -on: - workflow_dispatch: - push: - branches: [main, master, 'release*'] - tags: ['*'] - pull_request: - paths: - - 'misc/test-stubgenc.sh' - - 'mypy/stubgenc.py' - - 'mypy/stubdoc.py' - - 'mypy/stubutil.py' - - 'test-data/stubgen/**' - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - stubgenc: - # Check stub file generation for a small pybind11 project - # (full text match is required to pass) - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Setup 🐍 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Test stubgenc - run: misc/test-stubgenc.sh From a873c67e0b10cdedc5cdd1a05a47d0278ede23e5 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:14:00 +0800 Subject: [PATCH 13/17] Change workflows for this fork only (?) --- .github/workflows/build_wheels.yml | 25 --------------- .github/workflows/docs.yml | 48 ----------------------------- .github/workflows/sync_typeshed.yml | 36 ---------------------- .github/workflows/test.yml | 2 +- .github/workflows/test_stubgenc.yml | 41 ------------------------ 5 files changed, 1 insertion(+), 151 deletions(-) delete mode 100644 .github/workflows/build_wheels.yml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/sync_typeshed.yml delete mode 100644 .github/workflows/test_stubgenc.yml diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml deleted file mode 100644 index dae4937d5081..000000000000 --- a/.github/workflows/build_wheels.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Trigger wheel build - -on: - push: - branches: [main, master, 'release*'] - tags: ['*'] - -permissions: - contents: read - -jobs: - build-wheels: - if: github.repository == 'python/mypy' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Trigger script - env: - WHEELS_PUSH_TOKEN: ${{ secrets.WHEELS_PUSH_TOKEN }} - run: ./misc/trigger_wheel_build.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 3e78bf51913e..000000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Check documentation build - -on: - workflow_dispatch: - push: - branches: [main, master, 'release*'] - tags: ['*'] - pull_request: - paths: - - 'docs/**' - # We now have a docs check that fails if any error codes don't have documentation, - # so it's important to do the docs build on all PRs touching mypy/errorcodes.py - # in case somebody's adding a new error code without any docs - - 'mypy/errorcodes.py' - - 'mypyc/doc/**' - - '**/*.rst' - - '**/*.md' - - CREDITS - - LICENSE - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - docs: - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - TOXENV: docs - TOX_SKIP_MISSING_INTERPRETERS: False - VERIFY_MYPY_ERROR_CODES: 1 - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install tox - run: pip install tox==4.26.0 - - name: Setup tox environment - run: tox run -e ${{ env.TOXENV }} --notest - - name: Test - run: tox run -e ${{ env.TOXENV }} --skip-pkg-install diff --git a/.github/workflows/sync_typeshed.yml b/.github/workflows/sync_typeshed.yml deleted file mode 100644 index 2d5361a5919c..000000000000 --- a/.github/workflows/sync_typeshed.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Sync typeshed - -on: - workflow_dispatch: - schedule: - - cron: "0 0 1,15 * *" - -permissions: {} - -jobs: - sync_typeshed: - name: Sync typeshed - if: github.repository == 'python/mypy' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true # needed to `git push` the PR branch - # TODO: use whatever solution ends up working for - # https://github.com/python/typeshed/issues/8434 - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: git config - run: | - git config --global user.name mypybot - git config --global user.email '<>' - - name: Sync typeshed - run: | - python -m pip install requests==2.28.1 - GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} python misc/sync-typeshed.py --make-pr diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 362c61c58a99..ed5036557f7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Tests on: workflow_dispatch: push: - branches: [main, master, 'release*'] + branches: [main, master, 'release*', color-output-option] tags: ['*'] pull_request: paths-ignore: diff --git a/.github/workflows/test_stubgenc.yml b/.github/workflows/test_stubgenc.yml deleted file mode 100644 index 4676acf8695b..000000000000 --- a/.github/workflows/test_stubgenc.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Test stubgenc on pybind11_fixtures - -on: - workflow_dispatch: - push: - branches: [main, master, 'release*'] - tags: ['*'] - pull_request: - paths: - - 'misc/test-stubgenc.sh' - - 'mypy/stubgenc.py' - - 'mypy/stubdoc.py' - - 'mypy/stubutil.py' - - 'test-data/stubgen/**' - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - stubgenc: - # Check stub file generation for a small pybind11 project - # (full text match is required to pass) - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Setup 🐍 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Test stubgenc - run: misc/test-stubgenc.sh From af56737f369cbf714313f3c6b8171ece97f738e3 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:24:05 +0800 Subject: [PATCH 14/17] ... Currently the forked repo is messed up, I plan to test in fork repo's GitHub Actions then clean up the changes. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 65f67aba42a2..6f87e1195e2f 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r test-requirements.txt # This is a bit of a hack, but ensures the faster-cache path is tested in CI orjson;python_version=='3.12' -commands = python -m pytest {posargs} +commands = python -m pytest {posargs} -k daemon [testenv:dev] description = generate a DEV environment, that has all project libraries From 0fd4b98827f33882499728c8b145894d14ea6522 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:02:15 +0800 Subject: [PATCH 15/17] !!! --- mypy/dmypy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 0074a9b16f25..5b074b7f0ac9 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -855,7 +855,7 @@ def pretty_messages( True if util.should_force_color() else ( - self.formatter.default_colored + is_tty if self.options.color_output == "auto" else bool(self.options.color_output) ) From 3e101fe0f79eb53c77b68f931d79f0f3d5cc1cb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:03:24 +0000 Subject: [PATCH 16/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/dmypy_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 5b074b7f0ac9..5d5581315f56 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -855,9 +855,7 @@ def pretty_messages( True if util.should_force_color() else ( - is_tty - if self.options.color_output == "auto" - else bool(self.options.color_output) + is_tty if self.options.color_output == "auto" else bool(self.options.color_output) ) ) fit_width = self.options.pretty and is_tty From 1bce878fc96cdd723468c6edd9331406ac0f11d1 Mon Sep 17 00:00:00 2001 From: studyingegret <110317665+studyingegret@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:06:45 +0800 Subject: [PATCH 17/17] Revert changed workflow files --- .github/workflows/build_wheels.yml | 25 +++++++++++++++ .github/workflows/docs.yml | 48 +++++++++++++++++++++++++++++ .github/workflows/sync_typeshed.yml | 36 ++++++++++++++++++++++ .github/workflows/test.yml | 2 +- .github/workflows/test_stubgenc.yml | 41 ++++++++++++++++++++++++ tox.ini | 2 +- 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build_wheels.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/sync_typeshed.yml create mode 100644 .github/workflows/test_stubgenc.yml diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml new file mode 100644 index 000000000000..dae4937d5081 --- /dev/null +++ b/.github/workflows/build_wheels.yml @@ -0,0 +1,25 @@ +name: Trigger wheel build + +on: + push: + branches: [main, master, 'release*'] + tags: ['*'] + +permissions: + contents: read + +jobs: + build-wheels: + if: github.repository == 'python/mypy' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Trigger script + env: + WHEELS_PUSH_TOKEN: ${{ secrets.WHEELS_PUSH_TOKEN }} + run: ./misc/trigger_wheel_build.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000000..3e78bf51913e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,48 @@ +name: Check documentation build + +on: + workflow_dispatch: + push: + branches: [main, master, 'release*'] + tags: ['*'] + pull_request: + paths: + - 'docs/**' + # We now have a docs check that fails if any error codes don't have documentation, + # so it's important to do the docs build on all PRs touching mypy/errorcodes.py + # in case somebody's adding a new error code without any docs + - 'mypy/errorcodes.py' + - 'mypyc/doc/**' + - '**/*.rst' + - '**/*.md' + - CREDITS + - LICENSE + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + docs: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + TOXENV: docs + TOX_SKIP_MISSING_INTERPRETERS: False + VERIFY_MYPY_ERROR_CODES: 1 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install tox + run: pip install tox==4.26.0 + - name: Setup tox environment + run: tox run -e ${{ env.TOXENV }} --notest + - name: Test + run: tox run -e ${{ env.TOXENV }} --skip-pkg-install diff --git a/.github/workflows/sync_typeshed.yml b/.github/workflows/sync_typeshed.yml new file mode 100644 index 000000000000..2d5361a5919c --- /dev/null +++ b/.github/workflows/sync_typeshed.yml @@ -0,0 +1,36 @@ +name: Sync typeshed + +on: + workflow_dispatch: + schedule: + - cron: "0 0 1,15 * *" + +permissions: {} + +jobs: + sync_typeshed: + name: Sync typeshed + if: github.repository == 'python/mypy' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true # needed to `git push` the PR branch + # TODO: use whatever solution ends up working for + # https://github.com/python/typeshed/issues/8434 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: git config + run: | + git config --global user.name mypybot + git config --global user.email '<>' + - name: Sync typeshed + run: | + python -m pip install requests==2.28.1 + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} python misc/sync-typeshed.py --make-pr diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed5036557f7a..362c61c58a99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Tests on: workflow_dispatch: push: - branches: [main, master, 'release*', color-output-option] + branches: [main, master, 'release*'] tags: ['*'] pull_request: paths-ignore: diff --git a/.github/workflows/test_stubgenc.yml b/.github/workflows/test_stubgenc.yml new file mode 100644 index 000000000000..4676acf8695b --- /dev/null +++ b/.github/workflows/test_stubgenc.yml @@ -0,0 +1,41 @@ +name: Test stubgenc on pybind11_fixtures + +on: + workflow_dispatch: + push: + branches: [main, master, 'release*'] + tags: ['*'] + pull_request: + paths: + - 'misc/test-stubgenc.sh' + - 'mypy/stubgenc.py' + - 'mypy/stubdoc.py' + - 'mypy/stubutil.py' + - 'test-data/stubgen/**' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + stubgenc: + # Check stub file generation for a small pybind11 project + # (full text match is required to pass) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup 🐍 3.9 + uses: actions/setup-python@v5 + with: + python-version: 3.9 + + - name: Test stubgenc + run: misc/test-stubgenc.sh diff --git a/tox.ini b/tox.ini index 6f87e1195e2f..65f67aba42a2 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r test-requirements.txt # This is a bit of a hack, but ensures the faster-cache path is tested in CI orjson;python_version=='3.12' -commands = python -m pytest {posargs} -k daemon +commands = python -m pytest {posargs} [testenv:dev] description = generate a DEV environment, that has all project libraries