From f94fc8ff7070f0c80ed08228958794f233b3ae36 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 14:52:57 +0400 Subject: [PATCH 01/14] feat: Enable --pretty by default. Change the default mypy output to be pretty-printed, providing users with more readable error messages that include code context out-of-the-box. This commit implements the core logic for this feature: - In 'mypy/options.py', the default value for 'pretty' is set to 'True'. - In 'mypy/main.py', the command-line flags are reconfigured: - '--no-pretty' is now the primary, user-facing flag to disable the pretty printing. - The old '--pretty' flag is deprecated and hidden from help text to reflect that the pretty output is now default. This change intentionally breaks the test suite, which will be fixediIn subsequent commits by adapting the test runners. Part of #19108 --- mypy/main.py | 20 ++++++++++++-------- mypy/options.py | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index a407a88d3ac1..10b0647e2ec5 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -991,14 +991,6 @@ def add_invertible_flag( help="Show links to error code documentation", group=error_group, ) - add_invertible_flag( - "--pretty", - default=False, - help="Use visually nicer output in error messages:" - " Use soft word wrap, show source code snippets," - " and show error location markers", - group=error_group, - ) add_invertible_flag( "--no-color-output", dest="color_output", @@ -1026,6 +1018,18 @@ def add_invertible_flag( dest="many_errors_threshold", help=argparse.SUPPRESS, ) + error_group.add_argument( + '--no-pretty', + action='store_false', + dest='pretty', + help='Disable pretty error messages (pretty is now the default).' + ) + error_group.add_argument( + '--pretty', + action='store_true', + dest='pretty', + help=argparse.SUPPRESS + ) incremental_group = parser.add_argument_group( title="Incremental mode", diff --git a/mypy/options.py b/mypy/options.py index 4a89ef529c07..35390183199c 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -357,7 +357,7 @@ def __init__(self) -> None: self.hide_error_codes = False self.show_error_code_links = False # Use soft word wrap and show trimmed source snippets with error location markers. - self.pretty = False + self.pretty = True self.dump_graph = False self.dump_deps = False self.logical_deps = False From 54a0196409880736eba034f6ee8c86f39e556f66 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:00:48 +0400 Subject: [PATCH 02/14] fix(tests): Adapt TypeCheckSuite to default to non-pretty output This commit adapts the main test runner (`TypeCheckSuite`) to handle the new pretty-by-default behaviour. - For tests with a `# flags: ` line `--no-pretty` is now injected unless `--pretty` is explicitly requested. - For tests without any flags, `options.pretty` is directly set to `False` to ensure plain output. This strategy avoids modifying thousands of individual test-data files. --- mypy/test/helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index ae432ff6981b..aaedcd08d854 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -342,6 +342,8 @@ def parse_options( if flags: flag_list = flags.group(1).split() + if '--pretty' not in flag_list: + flag_list.append('--no-pretty') flag_list.append("--no-site-packages") # the tests shouldn't need an installed Python targets, options = process_options(flag_list, require_targets=False) if targets: @@ -350,11 +352,12 @@ def parse_options( if "--show-error-codes" not in flag_list: options.hide_error_codes = True else: - flag_list = [] options = Options() + options.pretty = False options.error_summary = False options.hide_error_codes = True options.force_union_syntax = True + flag_list = ['--no-pretty'] # Allow custom python version to override testfile_pyversion. if all(flag.split("=")[0] != "--python-version" for flag in flag_list): From e0d968e1dee3fe64505c42556c5eab8d35455414 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:06:09 +0400 Subject: [PATCH 03/14] fix(tests): Adapt CmdlineSuite to default to non-pretty output This commit adapts the `CmdlineSuite` runner to handle the new pretty-by-default behavior. The `parse_args` helper in `mypy/test/testcmdline.py` is modified to inject the `--no-pretty` flag into the arguments parsed from '# cmd:' lines in test cases. This ensures that command-line-driven tests, which run in a separate subprocess and bypass other test helpers, are also executed in non-pretty mode by default. Part of #19108. --- mypy/test/testcmdline.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 11d229042978..4f539abfe80d 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -131,8 +131,13 @@ def parse_args(line: str) -> list[str]: """ m = re.match("# cmd: mypy (.*)$", line) if not m: - return [] # No args; mypy will spit out an error. - return m.group(1).split() + return ['--no-pretty'] # No args; mypy will spit out an error. + args = m.group(1).split() + + if '--pretty' not in args: + args.append('--no-pretty') + + return args def parse_cwd(line: str) -> str | None: From 201f4368f2ebac5162ef3a26990d24da80a6304d Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:09:53 +0400 Subject: [PATCH 04/14] fix(tests): Adapt pythoneval suite to default to non-pretty output This commit adapts the `pythoneval` test runner to handle the new pretty-by-default behavior. The `test_python_evaluation` function in `mypy/test/testpythoneval.py` now injects the `--no-pretty` flag by default into the mypy command line it constructs. Logic is also included to respect a `# flags: --pretty` directive, ensuring that tests designed to check pretty-printing still work correctly. Part of #19108. --- mypy/test/testpythoneval.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py index 6d22aca07da7..35600b9c2ee8 100644 --- a/mypy/test/testpythoneval.py +++ b/mypy/test/testpythoneval.py @@ -54,6 +54,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None "--allow-empty-bodies", "--test-env", # Speeds up some checks ] + + mypy_cmdline.append("--no-pretty") + interpreter = python3_path mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}") @@ -69,6 +72,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None sys.version_info.minor, ): return + if '--pretty' in additional_flags: + mypy_cmdline.remove('--no-pretty') + mypy_cmdline.extend(additional_flags) # Write the program to a file. From 9d498a89af8905609853adcefb25da3620636b49 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:23:23 +0400 Subject: [PATCH 05/14] fix(tests): Adapt errorstream suite to default to non-pretty output This commit modifies the `test_error_stream` runner to set `options.pretty = False`, ensuring its tests continue to pass with the new pretty-by-default behavior. Part of #19108. --- mypy/test/testerrorstream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/test/testerrorstream.py b/mypy/test/testerrorstream.py index a54a3495ddb2..3edfe308789f 100644 --- a/mypy/test/testerrorstream.py +++ b/mypy/test/testerrorstream.py @@ -25,6 +25,7 @@ def test_error_stream(testcase: DataDrivenTestCase) -> None: The argument contains the description of the test case. """ options = Options() + options.pretty = False options.show_traceback = True options.hide_error_codes = True From 0a9e3269040e9fbf87778550e35151a2c6156d6a Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:30:52 +0400 Subject: [PATCH 06/14] fix(tests): Adapt DaemonSuite to handle new pretty default This commit adapts the `DaemonSuite` test runner and its corresponding data files to accommodate the new pretty-by-default behavior. This fix is nuanced due to the stateful, client-server nature of the daemon and required a two-part approach: 1. **Central Logic:** The `run_cmd` helper in `mypy/test/testdaemon.py` was modified to inject `--no-pretty` only into state-setting subcommands (`run`, `check`). This handles the majority of daemon tests. 2. **Manual Overrides:** A number of test cases in `daemon.test` were manually updated. This was necessary for tests that start the daemon with `dmypy start` or have other unique requirements, ensuring the server is correctly configured in non-pretty mode from the outset. This combined strategy ensures the entire daemon test suite passes. Part of #19108. --- mypy/test/testdaemon.py | 13 +++ test-data/unit/daemon.test | 222 ++++++++++++++++++------------------- 2 files changed, 124 insertions(+), 111 deletions(-) diff --git a/mypy/test/testdaemon.py b/mypy/test/testdaemon.py index 7115e682e60d..d93cfb929f33 100644 --- a/mypy/test/testdaemon.py +++ b/mypy/test/testdaemon.py @@ -79,6 +79,19 @@ def parse_script(input: list[str]) -> list[list[str]]: def run_cmd(input: str) -> tuple[int, str]: if input[1:].startswith("mypy run --") and "--show-error-codes" not in input: input += " --hide-error-codes" + is_pretty_test = "# NO-MODIFY" in input + modified_input = input.replace('# NO-MODIFY', '').strip() + cond1 = '--pretty' not in modified_input + cond2 = modified_input.startswith("dmypy run") or \ + modified_input.startswith("dmypy check ") + cond3 = not is_pretty_test + if cond1 and cond2 and cond3: + parts = modified_input.split(' ', 2) + command, subcommand = parts[0], parts[1] + args = parts[2] if len(parts) > 2 else "" + input = f"{command} {subcommand} {args} --no-pretty".strip() + else: + input = modified_input if input.startswith("dmypy "): input = sys.executable + " -m mypy." + input if input.startswith("mypy "): diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index 295eb4000d81..0e544ec8fa78 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -8,13 +8,13 @@ $ dmypy stop Daemon stopped [case testDaemonBasic] -$ dmypy start -- --follow-imports=error +$ dmypy start -- --follow-imports=error # NO-MODIFY Daemon started -$ dmypy check -- foo.py +$ dmypy check -- foo.py # NO-MODIFY Success: no issues found in 1 source file -$ dmypy recheck +$ dmypy recheck # NO-MODIFY Success: no issues found in 1 source file -$ dmypy stop +$ dmypy stop # NO-MODIFY Daemon stopped [file foo.py] def f(): pass @@ -229,17 +229,17 @@ Daemon has died == Return code: 2 [case testDaemonRecheck] -$ dmypy start -- --follow-imports=error --no-error-summary +$ dmypy start -- --follow-imports=error --no-error-summary --no-pretty Daemon started -$ dmypy check foo.py bar.py -$ dmypy recheck -$ dmypy recheck --update foo.py --remove bar.py sir_not_appearing_in_this_film.py +$ dmypy check foo.py bar.py # NO-MODIFY +$ dmypy recheck # NO-MODIFY +$ dmypy recheck --update foo.py --remove bar.py sir_not_appearing_in_this_film.py # NO-MODIFY foo.py:1: error: Import of "bar" ignored [misc] foo.py:1: note: (Using --follow-imports=error, module not passed on command line) == Return code: 1 -$ dmypy recheck --update bar.py -$ dmypy recheck --update sir_not_appearing_in_this_film.py -$ dmypy recheck --update --remove +$ dmypy recheck --update bar.py # NO-MODIFY +$ dmypy recheck --update sir_not_appearing_in_this_film.py # NO-MODIFY +$ dmypy recheck --update --remove # NO-MODIFY $ dmypy stop Daemon stopped [file foo.py] @@ -264,26 +264,26 @@ $ dmypy stop Daemon stopped [case testDaemonRunTwoFilesFullTypeshed] -$ dmypy run x.py +$ dmypy run x.py # NO-MODIFY Daemon started Success: no issues found in 1 source file -$ dmypy run y.py +$ dmypy run y.py # NO-MODIFY Success: no issues found in 1 source file -$ dmypy run x.py +$ dmypy run x.py # NO-MODIFY Success: no issues found in 1 source file [file x.py] [file y.py] [case testDaemonCheckTwoFilesFullTypeshed] -$ dmypy start +$ dmypy start -- --no-pretty Daemon started -$ dmypy check foo.py +$ dmypy check foo.py # NO-MODIFY foo.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] Found 1 error in 1 file (checked 1 source file) == Return code: 1 -$ dmypy check bar.py +$ dmypy check bar.py # NO-MODIFY Success: no issues found in 1 source file -$ dmypy check foo.py +$ dmypy check foo.py # NO-MODIFY foo.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] Found 1 error in 1 file (checked 1 source file) == Return code: 1 @@ -344,34 +344,34 @@ $ {python} -c "import sys; sys.stdout.write(open('log').read())" $ {python} -c "x = open('.mypy_cache/3.11/bar.meta.json').read(); y = open('asdf.json').read(); assert x == y" [case testDaemonSuggest] -$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary +$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --no-pretty Daemon started -$ dmypy suggest foo:foo +$ dmypy suggest foo:foo # NO-MODIFY Command 'suggest' is only valid after a 'check' command (that produces no parse errors) == Return code: 2 -$ dmypy check foo.py bar.py -$ dmypy suggest foo.bar +$ dmypy check foo.py bar.py # NO-MODIFY +$ dmypy suggest foo.bar # NO-MODIFY Unknown function foo.bar == Return code: 2 -$ dmypy suggest foo.var +$ dmypy suggest foo.var # NO-MODIFY Object foo.var is not a function == Return code: 2 -$ dmypy suggest foo.Foo.var +$ dmypy suggest foo.Foo.var # NO-MODIFY Unknown class foo.Foo == Return code: 2 -$ dmypy suggest foo.Bar.baz +$ dmypy suggest foo.Bar.baz # NO-MODIFY Unknown method foo.Bar.baz == Return code: 2 -$ dmypy suggest foo.foo.baz +$ dmypy suggest foo.foo.baz # NO-MODIFY Object foo.foo is not a class == Return code: 2 -$ dmypy suggest --callsites foo.foo +$ dmypy suggest --callsites foo.foo # NO-MODIFY bar.py:3: (str) bar.py:4: (arg=str) -$ dmypy suggest foo.foo +$ dmypy suggest foo.foo # NO-MODIFY (str) -> int -$ {python} -c "import shutil; shutil.copy('foo2.py', 'foo.py')" -$ dmypy check foo.py bar.py +$ {python} -c "import shutil; shutil.copy('foo2.py', 'foo.py')" # NO-MODIFY +$ dmypy check foo.py bar.py # NO-MODIFY bar.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] == Return code: 1 [file foo.py] @@ -393,26 +393,26 @@ def bar() -> None: foo(arg='xyz') [case testDaemonInspectCheck] -$ dmypy start +$ dmypy start # NO-MODIFY Daemon started -$ dmypy check foo.py +$ dmypy check foo.py # NO-MODIFY Success: no issues found in 1 source file -$ dmypy check foo.py --export-types +$ dmypy check foo.py --export-types # NO-MODIFY Success: no issues found in 1 source file -$ dmypy inspect foo.py:1:1 +$ dmypy inspect foo.py:1:1 # NO-MODIFY "int" [file foo.py] x = 1 [case testDaemonInspectRun] -$ dmypy run test1.py +$ dmypy run test1.py # NO-MODIFY Daemon started Success: no issues found in 1 source file -$ dmypy run test2.py +$ dmypy run test2.py # NO-MODIFY Success: no issues found in 1 source file -$ dmypy run test1.py --export-types +$ dmypy run test1.py --export-types # NO-MODIFY Success: no issues found in 1 source file -$ dmypy inspect test1.py:1:1 +$ dmypy inspect test1.py:1:1 # NO-MODIFY "int" [file test1.py] a: int @@ -420,49 +420,49 @@ a: int a: str [case testDaemonGetType] -$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --python-version 3.9 +$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --python-version 3.9 --no-pretty Daemon started -$ dmypy inspect foo:1:2:3:4 +$ dmypy inspect foo:1:2:3:4 # NO-MODIFY Command "inspect" is only valid after a "check" command (that produces no parse errors) == Return code: 2 -$ dmypy check foo.py --export-types +$ dmypy check foo.py --export-types # NO-MODIFY foo.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] == Return code: 1 -$ dmypy inspect foo:1 +$ dmypy inspect foo:1 # NO-MODIFY Format should be file:line:column[:end_line:end_column] == Return code: 2 -$ dmypy inspect foo:1:2:3 +$ dmypy inspect foo:1:2:3 # NO-MODIFY Source file is not a Python file == Return code: 2 -$ dmypy inspect foo.py:1:2:a:b +$ dmypy inspect foo.py:1:2:a:b # NO-MODIFY invalid literal for int() with base 10: 'a' == Return code: 2 -$ dmypy inspect foo.pyc:1:1:2:2 +$ dmypy inspect foo.pyc:1:1:2:2 # NO-MODIFY Source file is not a Python file == Return code: 2 -$ dmypy inspect bar/baz.py:1:1:2:2 +$ dmypy inspect bar/baz.py:1:1:2:2 # NO-MODIFY Unknown module: bar/baz.py == Return code: 1 -$ dmypy inspect foo.py:3:1:1:1 +$ dmypy inspect foo.py:3:1:1:1 # NO-MODIFY "end_line" must not be before "line" == Return code: 2 -$ dmypy inspect foo.py:3:3:3:1 +$ dmypy inspect foo.py:3:3:3:1 # NO-MODIFY "end_column" must be after "column" == Return code: 2 -$ dmypy inspect foo.py:3:10:3:17 +$ dmypy inspect foo.py:3:10:3:17 # NO-MODIFY "str" -$ dmypy inspect foo.py:3:10:3:17 -vv +$ dmypy inspect foo.py:3:10:3:17 -vv # NO-MODIFY "builtins.str" -$ dmypy inspect foo.py:9:9:9:11 +$ dmypy inspect foo.py:9:9:9:11 # NO-MODIFY "int" -$ dmypy inspect foo.py:11:1:11:3 +$ dmypy inspect foo.py:11:1:11:3 # NO-MODIFY "Callable[[Optional[int]], None]" -$ dmypy inspect foo.py:11:1:13:1 +$ dmypy inspect foo.py:11:1:13:1 # NO-MODIFY "None" -$ dmypy inspect foo.py:1:2:3:4 +$ dmypy inspect foo.py:1:2:3:4 # NO-MODIFY Can't find expression at span 1:2:3:4 == Return code: 1 -$ dmypy inspect foo.py:17:5:17:5 +$ dmypy inspect foo.py:17:5:17:5 # NO-MODIFY No known type available for "NameExpr" (maybe unreachable or try --force-reload) == Return code: 1 @@ -486,43 +486,43 @@ def unreachable(x: int) -> None: x # line 17 [case testDaemonGetTypeInexact] -$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary +$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --no-pretty Daemon started -$ dmypy check foo.py --export-types -$ dmypy inspect foo.py:1:a +$ dmypy check foo.py --export-types # NO-MODIFY +$ dmypy inspect foo.py:1:a # NO-MODIFY invalid literal for int() with base 10: 'a' == Return code: 2 -$ dmypy inspect foo.pyc:1:2 +$ dmypy inspect foo.pyc:1:2 # NO-MODIFY Source file is not a Python file == Return code: 2 -$ dmypy inspect bar/baz.py:1:2 +$ dmypy inspect bar/baz.py:1:2 # NO-MODIFY Unknown module: bar/baz.py == Return code: 1 -$ dmypy inspect foo.py:7:5 --include-span +$ dmypy inspect foo.py:7:5 --include-span # NO-MODIFY 7:5:7:5 -> "int" 7:5:7:11 -> "int" 7:1:7:12 -> "None" -$ dmypy inspect foo.py:7:5 --include-kind +$ dmypy inspect foo.py:7:5 --include-kind # NO-MODIFY NameExpr -> "int" OpExpr -> "int" CallExpr -> "None" -$ dmypy inspect foo.py:7:5 --include-span --include-kind +$ dmypy inspect foo.py:7:5 --include-span --include-kind # NO-MODIFY NameExpr:7:5:7:5 -> "int" OpExpr:7:5:7:11 -> "int" CallExpr:7:1:7:12 -> "None" -$ dmypy inspect foo.py:7:5 -vv +$ dmypy inspect foo.py:7:5 -vv # NO-MODIFY "builtins.int" "builtins.int" "None" -$ dmypy inspect foo.py:7:5 -vv --limit=1 +$ dmypy inspect foo.py:7:5 -vv --limit=1 # NO-MODIFY "builtins.int" -$ dmypy inspect foo.py:7:3 +$ dmypy inspect foo.py:7:3 # NO-MODIFY "Callable[[int], None]" "None" -$ dmypy inspect foo.py:1:2 +$ dmypy inspect foo.py:1:2 # NO-MODIFY Can't find any expressions at position 1:2 == Return code: 1 -$ dmypy inspect foo.py:11:5 --force-reload +$ dmypy inspect foo.py:11:5 --force-reload # NO-MODIFY No known type available for "NameExpr" (maybe unreachable) No known type available for "OpExpr" (maybe unreachable) == Return code: 1 @@ -541,20 +541,20 @@ def unreachable(x: int, y: int) -> None: x and y # line 11 [case testDaemonGetAttrs] -$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary +$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --no-pretty Daemon started -$ dmypy check foo.py bar.py --export-types -$ dmypy inspect foo.py:9:1 --show attrs --include-span --include-kind -vv +$ dmypy check foo.py bar.py --export-types # NO-MODIFY +$ dmypy inspect foo.py:9:1 --show attrs --include-span --include-kind -vv # NO-MODIFY NameExpr:9:1:9:1 -> {"foo.C": ["a", "x", "y"], "foo.B": ["a", "b"]} -$ dmypy inspect foo.py:11:10 --show attrs +$ dmypy inspect foo.py:11:10 --show attrs # NO-MODIFY No known type available for "StrExpr" (maybe unreachable or try --force-reload) == Return code: 1 -$ dmypy inspect foo.py:1:1 --show attrs +$ dmypy inspect foo.py:1:1 --show attrs # NO-MODIFY Can't find any expressions at position 1:1 == Return code: 1 -$ dmypy inspect --show attrs bar.py:10:1 +$ dmypy inspect --show attrs bar.py:10:1 # NO-MODIFY {"A": ["z"], "B": ["z"]} -$ dmypy inspect --show attrs bar.py:10:1 --union-attrs +$ dmypy inspect --show attrs bar.py:10:1 --union-attrs # NO-MODIFY {"A": ["x", "z"], "B": ["y", "z"]} [file foo.py] @@ -583,24 +583,24 @@ var: Union[A, B] var # line 10 [case testDaemonGetDefinition] -$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary +$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary # NO-MODIFY Daemon started -$ dmypy check foo.py bar/baz.py bar/__init__.py --export-types -$ dmypy inspect foo.py:5:1 --show definition +$ dmypy check foo.py bar/baz.py bar/__init__.py --export-types # NO-MODIFY +$ dmypy inspect foo.py:5:1 --show definition # NO-MODIFY foo.py:4:1:y -$ dmypy inspect foo.py:2:3 --show definition --include-span --include-kind -vv +$ dmypy inspect foo.py:2:3 --show definition --include-span --include-kind -vv # NO-MODIFY MemberExpr:2:1:2:7 -> bar/baz.py:3:5:Alias -$ dmypy inspect foo.py:3:1 --show definition +$ dmypy inspect foo.py:3:1 --show definition # NO-MODIFY Cannot find definition for "NameExpr" at 3:1:3:1 == Return code: 1 -$ dmypy inspect foo.py:4:6 --show definition +$ dmypy inspect foo.py:4:6 --show definition # NO-MODIFY No name or member expressions at 4:6 == Return code: 1 -$ dmypy inspect foo.py:7:1:7:6 --show definition +$ dmypy inspect foo.py:7:1:7:6 --show definition # NO-MODIFY bar/baz.py:4:5:attr -$ dmypy inspect foo.py:10:10 --show definition --include-span +$ dmypy inspect foo.py:10:10 --show definition --include-span # NO-MODIFY 10:1:10:12 -> bar/baz.py:6:1:test -$ dmypy inspect foo.py:14:6 --show definition --include-span --include-kind +$ dmypy inspect foo.py:14:6 --show definition --include-span --include-kind # NO-MODIFY NameExpr:14:5:14:7 -> foo.py:13:9:arg MemberExpr:14:5:14:9 -> bar/baz.py:9:5:x, bar/baz.py:11:5:x @@ -635,12 +635,12 @@ class B: x: int [case testDaemonInspectSelectCorrectFile] -$ dmypy run test.py --export-types +$ dmypy run test.py --export-types # NO-MODIFY Daemon started Success: no issues found in 1 source file -$ dmypy inspect demo/test.py:1:1 +$ dmypy inspect demo/test.py:1:1 # NO-MODIFY "int" -$ dmypy inspect test.py:1:1 +$ dmypy inspect test.py:1:1 # NO-MODIFY "str" [file test.py] b: str @@ -650,12 +650,12 @@ a: int [case testUnusedTypeIgnorePreservedOnRerun] -- Regression test for https://github.com/python/mypy/issues/9655 -$ dmypy start -- --warn-unused-ignores --no-error-summary --hide-error-codes +$ dmypy start -- --warn-unused-ignores --no-error-summary --hide-error-codes --no-pretty Daemon started -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:2: error: Unused "type: ignore" comment == Return code: 1 -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:2: error: Unused "type: ignore" comment == Return code: 1 @@ -667,12 +667,12 @@ a = 1 # type: ignore [case testTypeIgnoreWithoutCodePreservedOnRerun] -- Regression test for https://github.com/python/mypy/issues/9655 -$ dmypy start -- --enable-error-code ignore-without-code --no-error-summary +$ dmypy start -- --enable-error-code ignore-without-code --no-error-summary --no-pretty Daemon started -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:2: error: "type: ignore" comment without error code [ignore-without-code] == Return code: 1 -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:2: error: "type: ignore" comment without error code [ignore-without-code] == Return code: 1 @@ -684,12 +684,12 @@ a = 1 # type: ignore [case testPossiblyUndefinedVarsPreservedAfterRerun] -- Regression test for https://github.com/python/mypy/issues/9655 -$ dmypy start -- --enable-error-code possibly-undefined --no-error-summary +$ dmypy start -- --enable-error-code possibly-undefined --no-error-summary --no-pretty Daemon started -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:4: error: Name "a" may be undefined [possibly-undefined] == Return code: 1 -$ dmypy check -- bar.py +$ dmypy check -- bar.py # NO-MODIFY bar.py:4: error: Name "a" may be undefined [possibly-undefined] == Return code: 1 @@ -702,12 +702,12 @@ if False: a [case testUnusedTypeIgnorePreservedOnRerunWithIgnoredMissingImports] -$ dmypy start -- --no-error-summary --ignore-missing-imports --warn-unused-ignores +$ dmypy start -- --no-error-summary --ignore-missing-imports --warn-unused-ignores --no-pretty Daemon started -$ dmypy check foo +$ dmypy check foo # NO-MODIFY foo/main.py:3: error: Unused "type: ignore" comment [unused-ignore] == Return code: 1 -$ dmypy check foo +$ dmypy check foo # NO-MODIFY foo/main.py:3: error: Unused "type: ignore" comment [unused-ignore] == Return code: 1 @@ -723,12 +723,12 @@ from foo.does_not_exist import * a = 1 # type: ignore [case testModuleDoesNotExistPreservedOnRerun] -$ dmypy start -- --no-error-summary --ignore-missing-imports +$ dmypy start -- --no-error-summary --ignore-missing-imports --no-pretty Daemon started -$ dmypy check foo +$ dmypy check foo # NO-MODIFY foo/main.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined] == Return code: 1 -$ dmypy check foo +$ dmypy check foo # NO-MODIFY foo/main.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined] == Return code: 1 @@ -742,13 +742,13 @@ from unused.submodule import * [case testReturnTypeIgnoreAfterUnknownImport] -- Return type ignores after unknown imports and unused modules are respected on the second pass. -$ dmypy start -- --warn-unused-ignores --no-error-summary +$ dmypy start -- --warn-unused-ignores --no-error-summary --no-pretty Daemon started -$ dmypy check -- foo.py +$ dmypy check -- foo.py # NO-MODIFY foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports == Return code: 1 -$ dmypy check -- foo.py +$ dmypy check -- foo.py # NO-MODIFY foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports == Return code: 1 @@ -762,13 +762,13 @@ def is_foo() -> str: return True # type: ignore [case testAttrsTypeIgnoreAfterUnknownImport] -$ dmypy start -- --warn-unused-ignores --no-error-summary +$ dmypy start -- --warn-unused-ignores --no-error-summary --no-pretty Daemon started -$ dmypy check -- foo.py +$ dmypy check -- foo.py # NO-MODIFY foo.py:3: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] foo.py:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports == Return code: 1 -$ dmypy check -- foo.py +$ dmypy check -- foo.py # NO-MODIFY foo.py:3: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] foo.py:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports == Return code: 1 @@ -786,16 +786,16 @@ class A: self.__attrs_init__() # type: ignore[attr-defined] [case testDaemonImportAncestors] -$ dmypy run test.py +$ dmypy run -- --no-pretty --show-error-codes test.py # NO-MODIFY Daemon started test.py:2: error: Unsupported operand types for + ("int" and "str") [operator] Found 1 error in 1 file (checked 1 source file) == Return code: 1 -$ dmypy run test.py +$ dmypy run -- --no-pretty --show-error-codes test.py # NO-MODIFY test.py:2: error: Unsupported operand types for + ("int" and "str") [operator] Found 1 error in 1 file (checked 1 source file) == Return code: 1 -$ dmypy run test.py +$ dmypy run -- --no-pretty --show-error-codes test.py # NO-MODIFY test.py:2: error: Unsupported operand types for + ("int" and "str") [operator] Found 1 error in 1 file (checked 1 source file) == Return code: 1 From f07217d8075069f5cc70be3ce0fc9aac540c4732 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:33:01 +0400 Subject: [PATCH 07/14] fix(tests): Adapt PEP561Suite to default to non-pretty output This commit adapts the `PEP561Suite` test runner to handle the new pretty-by-default behavior. The `parse_mypy_args` helper in `mypy/test/testpep561.py` is modified to inject the `--no-pretty` flag into the arguments parsed from '# flags:' lines in test cases. Part of #19108. --- mypy/test/testpep561.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index 0afb69bc0c99..24532ebea641 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -174,5 +174,8 @@ def parse_pkgs(comment: str) -> tuple[list[str], list[str]]: def parse_mypy_args(line: str) -> list[str]: m = re.match("# flags: (.*)$", line) if not m: - return [] # No args; mypy will spit out an error. - return m.group(1).split() + return ['--no-pretty'] # No args; mypy will spit out an error. + args = m.group(1).split() + args.append('--no-pretty') + + return args From d4de5a74aab9dd235217f3a15561d7cf9fd51019 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:34:18 +0400 Subject: [PATCH 08/14] fix(tests): Update Stubtest unit tests to expect pretty output This commit adapts several unit tests in the `StubtestMiscUnit` suite to handle the new pretty-by-default behavior. Because the `stubtest` runner is a distinct command-line tool, injecting the `--no-pretty` flag was not feasible as it's an unrecognized argument. Instead, the `assert` statements in the failing tests have been updated to match the new, pretty-printed error output from mypy. Part of #19108. --- mypy/test/teststubtest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 7925f2a6bd3e..830a71e98d76 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -2581,13 +2581,17 @@ def test_mypy_build(self) -> None: output = run_stubtest(stub="+", runtime="", options=[]) assert output == ( "error: not checking stubs due to failed mypy compile:\n{}.pyi:1: " - "error: Invalid syntax [syntax]\n".format(TEST_MODULE_NAME) + "error: Invalid syntax [syntax]\n" + " +\n" + " ^\n".format(TEST_MODULE_NAME) ) output = run_stubtest(stub="def f(): ...\ndef f(): ...", runtime="", options=[]) assert output == ( "error: not checking stubs due to mypy build errors:\n{}.pyi:2: " - 'error: Name "f" already defined on line 1 [no-redef]\n'.format(TEST_MODULE_NAME) + 'error: Name "f" already defined on line 1 [no-redef]\n' + " def f(): ...\n" + " ^~~~~~~~~~~~\n".format(TEST_MODULE_NAME) ) def test_missing_stubs(self) -> None: @@ -2665,6 +2669,8 @@ def test_config_file_error_codes(self) -> None: assert output == ( "error: not checking stubs due to mypy build errors:\n" 'test_module.pyi:1: error: Name "SOME_GLOBAL_CONST" is not defined [name-defined]\n' + " temp = SOME_GLOBAL_CONST\n" + " ^~~~~~~~~~~~~~~~~\n" ) config_file = "[mypy]\ndisable_error_code = name-defined\n" From ce8cc299eeb299ca95697fcb3516ed44067fedb5 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 24 Jul 2025 15:35:25 +0400 Subject: [PATCH 09/14] docs: Update docs for pretty-by-default behavior This commit updates the user-facing documentation to reflect that pretty-printing is now the default output format. - The `--pretty` flag documentation in `command_line.rst` has been replaced with documentation for the new `--no-pretty` flag. - The `config_file.rst` documentation for the `pretty` option has been updated to state that its default is now `True`. Fixes #19108. --- docs/source/command_line.rst | 6 +++--- docs/source/config_file.rst | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 697e0fb69eed..611cfccd4849 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -910,10 +910,10 @@ in error messages. See :ref:`error-codes` for more information. -.. option:: --pretty +.. option:: --no-pretty - Use visually nicer output in error messages: use soft word wrap, - show source code snippets, and show error location markers. + Disable pretty error messages and use the old, plain output format. + Pretty-printing is enable by default. .. option:: --no-color-output diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index b4f134f26cb1..57263fe59ea0 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -896,10 +896,11 @@ These options may only be set in the global section (``[mypy]``). .. confval:: pretty :type: boolean - :default: False + :default: True Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location markers. + Pretty printing is enabled by default, but can be disabled. .. confval:: color_output From 7b79b5a4b2828c5b4435f2f7cf2deeba6795f544 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sat, 26 Jul 2025 01:11:35 +0400 Subject: [PATCH 10/14] style: Fix ruff linting error in testdaemon.py --- mypy/test/testdaemon.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/test/testdaemon.py b/mypy/test/testdaemon.py index d93cfb929f33..b22e95f2d4a3 100644 --- a/mypy/test/testdaemon.py +++ b/mypy/test/testdaemon.py @@ -82,8 +82,7 @@ def run_cmd(input: str) -> tuple[int, str]: is_pretty_test = "# NO-MODIFY" in input modified_input = input.replace('# NO-MODIFY', '').strip() cond1 = '--pretty' not in modified_input - cond2 = modified_input.startswith("dmypy run") or \ - modified_input.startswith("dmypy check ") + cond2 = modified_input.startswith(("dmypy run", "dmypy check")) cond3 = not is_pretty_test if cond1 and cond2 and cond3: parts = modified_input.split(' ', 2) From d67c3679eaea8497079950ee20f543029317fb50 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sat, 26 Jul 2025 02:35:32 +0400 Subject: [PATCH 11/14] chore: Re-trigger CI From afc658561be0cca1c227dea52be2bc71478bed56 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 06:06:08 +0000 Subject: [PATCH 12/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 13 +++++-------- mypy/test/helpers.py | 6 +++--- mypy/test/testcmdline.py | 8 ++++---- mypy/test/testdaemon.py | 6 +++--- mypy/test/testpep561.py | 4 ++-- mypy/test/testpythoneval.py | 6 +++--- test-data/unit/daemon.test | 4 ++-- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 10b0647e2ec5..050039518d46 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1019,16 +1019,13 @@ def add_invertible_flag( help=argparse.SUPPRESS, ) error_group.add_argument( - '--no-pretty', - action='store_false', - dest='pretty', - help='Disable pretty error messages (pretty is now the default).' + "--no-pretty", + action="store_false", + dest="pretty", + help="Disable pretty error messages (pretty is now the default).", ) error_group.add_argument( - '--pretty', - action='store_true', - dest='pretty', - help=argparse.SUPPRESS + "--pretty", action="store_true", dest="pretty", help=argparse.SUPPRESS ) incremental_group = parser.add_argument_group( diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index aaedcd08d854..20a1fae6c004 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -342,8 +342,8 @@ def parse_options( if flags: flag_list = flags.group(1).split() - if '--pretty' not in flag_list: - flag_list.append('--no-pretty') + if "--pretty" not in flag_list: + flag_list.append("--no-pretty") flag_list.append("--no-site-packages") # the tests shouldn't need an installed Python targets, options = process_options(flag_list, require_targets=False) if targets: @@ -357,7 +357,7 @@ def parse_options( options.error_summary = False options.hide_error_codes = True options.force_union_syntax = True - flag_list = ['--no-pretty'] + flag_list = ["--no-pretty"] # Allow custom python version to override testfile_pyversion. if all(flag.split("=")[0] != "--python-version" for flag in flag_list): diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 4f539abfe80d..0af3ced4c739 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -131,11 +131,11 @@ def parse_args(line: str) -> list[str]: """ m = re.match("# cmd: mypy (.*)$", line) if not m: - return ['--no-pretty'] # No args; mypy will spit out an error. + return ["--no-pretty"] # No args; mypy will spit out an error. args = m.group(1).split() - - if '--pretty' not in args: - args.append('--no-pretty') + + if "--pretty" not in args: + args.append("--no-pretty") return args diff --git a/mypy/test/testdaemon.py b/mypy/test/testdaemon.py index b22e95f2d4a3..78968b6e8a74 100644 --- a/mypy/test/testdaemon.py +++ b/mypy/test/testdaemon.py @@ -80,12 +80,12 @@ def run_cmd(input: str) -> tuple[int, str]: if input[1:].startswith("mypy run --") and "--show-error-codes" not in input: input += " --hide-error-codes" is_pretty_test = "# NO-MODIFY" in input - modified_input = input.replace('# NO-MODIFY', '').strip() - cond1 = '--pretty' not in modified_input + modified_input = input.replace("# NO-MODIFY", "").strip() + cond1 = "--pretty" not in modified_input cond2 = modified_input.startswith(("dmypy run", "dmypy check")) cond3 = not is_pretty_test if cond1 and cond2 and cond3: - parts = modified_input.split(' ', 2) + parts = modified_input.split(" ", 2) command, subcommand = parts[0], parts[1] args = parts[2] if len(parts) > 2 else "" input = f"{command} {subcommand} {args} --no-pretty".strip() diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index 24532ebea641..0ce2a09d364e 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -174,8 +174,8 @@ def parse_pkgs(comment: str) -> tuple[list[str], list[str]]: def parse_mypy_args(line: str) -> list[str]: m = re.match("# flags: (.*)$", line) if not m: - return ['--no-pretty'] # No args; mypy will spit out an error. + return ["--no-pretty"] # No args; mypy will spit out an error. args = m.group(1).split() - args.append('--no-pretty') + args.append("--no-pretty") return args diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py index 35600b9c2ee8..d01cf5711470 100644 --- a/mypy/test/testpythoneval.py +++ b/mypy/test/testpythoneval.py @@ -72,9 +72,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None sys.version_info.minor, ): return - if '--pretty' in additional_flags: - mypy_cmdline.remove('--no-pretty') - + if "--pretty" in additional_flags: + mypy_cmdline.remove("--no-pretty") + mypy_cmdline.extend(additional_flags) # Write the program to a file. diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index 0e544ec8fa78..283bb3400fe3 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -395,7 +395,7 @@ def bar() -> None: [case testDaemonInspectCheck] $ dmypy start # NO-MODIFY Daemon started -$ dmypy check foo.py # NO-MODIFY +$ dmypy check foo.py # NO-MODIFY Success: no issues found in 1 source file $ dmypy check foo.py --export-types # NO-MODIFY Success: no issues found in 1 source file @@ -489,7 +489,7 @@ def unreachable(x: int) -> None: $ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --no-pretty Daemon started $ dmypy check foo.py --export-types # NO-MODIFY -$ dmypy inspect foo.py:1:a # NO-MODIFY +$ dmypy inspect foo.py:1:a # NO-MODIFY invalid literal for int() with base 10: 'a' == Return code: 2 $ dmypy inspect foo.pyc:1:2 # NO-MODIFY From 23e598079f3c868daba15e2becc911ce8ad95179 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 31 Jul 2025 17:09:57 +0400 Subject: [PATCH 13/14] fix: Remove redundant `flag_list` in `parse_options` --- mypy/test/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 20a1fae6c004..045445cb7ec4 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -357,7 +357,7 @@ def parse_options( options.error_summary = False options.hide_error_codes = True options.force_union_syntax = True - flag_list = ["--no-pretty"] + flag_list = [] # Allow custom python version to override testfile_pyversion. if all(flag.split("=")[0] != "--python-version" for flag in flag_list): From 66121c9f901ddaab7bfc6a35c82214a40bf6d742 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 31 Jul 2025 17:17:29 +0400 Subject: [PATCH 14/14] refactor(cli): Simplify flag implementation using inverted `add_invertible_flag` --- mypy/main.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 050039518d46..9f9d367dea35 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -998,6 +998,13 @@ def add_invertible_flag( help="Do not colorize error messages", group=error_group, ) + add_invertible_flag( + "--no-pretty", + default=True, + dest="pretty", + help="Disable pretty error message", + group=error_group, + ) add_invertible_flag( "--no-error-summary", dest="error_summary", @@ -1018,15 +1025,6 @@ def add_invertible_flag( dest="many_errors_threshold", help=argparse.SUPPRESS, ) - error_group.add_argument( - "--no-pretty", - action="store_false", - dest="pretty", - help="Disable pretty error messages (pretty is now the default).", - ) - error_group.add_argument( - "--pretty", action="store_true", dest="pretty", help=argparse.SUPPRESS - ) incremental_group = parser.add_argument_group( title="Incremental mode",