diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b62f5cd58e44..351ee8fab3cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ ### Added - `TypeGuard` is retained in inferred types (#504) - Type narrowing is applied from lambda execution (#504) +- `--ide` flag (#501) ### Enhancements +- `show-error-context`/`pretty` are now on by default (#501) +- Show fake column number when `--show-error-end` (#501) ### Fixes - Don't show "X defined here" when error context is hidden (#498) - Fix issue with reveal code in ignore message (#490) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index a993d63d8aecc..3fed2d78c877b 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -156,6 +156,10 @@ You can read more :ref:`here `. Based features ************** +.. option:: --ide + + Preset options for best compatibility with an integrated developer environment. + .. option:: --default-return See :confval:`default_return`. diff --git a/mypy/dmypy/client.py b/mypy/dmypy/client.py index bd1242eeadeab..2fb4768efcce9 100644 --- a/mypy/dmypy/client.py +++ b/mypy/dmypy/client.py @@ -46,7 +46,7 @@ def __init__(self, prog: str) -> None: "-V", "--version", action="version", - version=f"Basedmypy Daemon {__based_version__}\nBased on %(prog)s {__version__}", + version=f"Basedmypy-Daemon {__based_version__}\nBased on %(prog)s {__version__}", help="Show program's version number and exit", ) subparsers = parser.add_subparsers() diff --git a/mypy/errors.py b/mypy/errors.py index 972b53e735acf..37a87d6119919 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -49,11 +49,12 @@ codes.NAME_DEFINED, # Overrides have a custom link to docs codes.OVERRIDE, + codes.REVEAL, } allowed_duplicates: Final = ["@overload", "Got:", "Expected:"] -BASE_RTD_URL: Final = "https://mypy.rtfd.io/en/stable/_refs.html#code" +BASE_RTD_URL: Final = "https://kotlinisland.github.io/basedmypy/_refs.html#code" # Keep track of the original error code when the error code of a message is changed. # This is used to give notes about out-of-date "type: ignore" comments. @@ -648,8 +649,9 @@ def add_error_info(self, info: ErrorInfo) -> None: and info.code not in HIDE_LINK_CODES ): message = f"See {BASE_RTD_URL}-{info.code.code} for more info" - if message in self.only_once_messages: + if not self.options.ide and message in self.only_once_messages: return + info_ = info self.only_once_messages.add(message) info = ErrorInfo( import_ctx=info.import_ctx, @@ -669,6 +671,7 @@ def add_error_info(self, info: ErrorInfo) -> None: allow_dups=False, priority=20, ) + info_.notes.append(info) self._add_error_info(file, info) def has_many_errors(self) -> bool: @@ -793,6 +796,7 @@ def generate_unused_ignore_errors(self, file: str) -> None: narrower = set(used_ignored_codes) & codes.sub_code_map[unused] if narrower: message += f", use narrower [{', '.join(narrower)}] instead of [{unused}] code" + # Don't use report since add_error_info will ignore the error! info = ErrorInfo( import_ctx=self.import_context(), @@ -1185,11 +1189,16 @@ def render_messages( ) ) src = ( - file == current_file - and source_lines - and e.line > 0 - and source_lines[e.line - 1].strip() + file == current_file and source_lines and e.line > 0 and source_lines[e.line - 1] ) or "" + # when there is no column, but we still want an ide to show an error + if e.column == -1 and self.options.show_error_end: + if src: + e.column = src.find(src.strip()) + e.end_column = len(src) + else: + e.column = 1 + src = src.strip() if isinstance(e.message, ErrorMessage): result.append( ( diff --git a/mypy/main.py b/mypy/main.py index ba555407ea851..a7c15069c10a9 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -611,6 +611,9 @@ def add_invertible_flag( "You probably want to set this on a module override", group=based_group, ) + add_invertible_flag( + "--ide", default=False, help="Best default for IDE integration.", group=based_group + ) config_group = parser.add_argument_group( title="Config file", @@ -940,8 +943,8 @@ def add_invertible_flag( description="Adjust the amount of detail shown in error messages.", ) add_invertible_flag( - "--show-error-context", - default=False, + "--hide-error-context", + default=True, dest="show_error_context", help='Precede errors with "note:" messages explaining context', group=error_group, @@ -966,14 +969,16 @@ def add_invertible_flag( group=error_group, ) add_invertible_flag( - "--show-error-code-links", - default=False, - help="Show links to error code documentation", + "--hide-error-code-links", + dest="show_error_code_links", + default=True, + help="Hide links to error code documentation", group=error_group, ) add_invertible_flag( - "--pretty", - default=False, + "--no-pretty", + default=True, + dest="pretty", help="Use visually nicer output in error messages:" " Use soft word wrap, show source code snippets," " and show error location markers", @@ -1309,6 +1314,7 @@ def add_invertible_flag( parser.error(f"Cannot find config file '{config_file}'") mypy.options._based = dummy.__dict__["special-opts:strict"] + mypy.options._legacy = os.getenv("__MYPY_UNDER_TEST__") == "2" based_enabled_codes = ( { @@ -1335,9 +1341,23 @@ def add_invertible_flag( def set_strict_flags() -> None: pass + def set_ide_flags() -> None: + for dest, value in { + "show_error_context": False, + "error_summary": False, + "pretty": False, + "show_error_end": True, + }.items(): + setattr(options, dest, value) + # Parse config file first, so command line can override. parse_config_file(options, set_strict_flags, config_file, stdout, stderr) + # Set IDE flags before parsing (if IDE mode enabled), so other command + # line options can override. + if dummy.ide: + set_ide_flags() + # Override cache_dir if provided in the environment environ_cache_dir = os.getenv("MYPY_CACHE_DIR", "") if environ_cache_dir.strip(): diff --git a/mypy/options.py b/mypy/options.py index e623f3de95833..4e415de087b26 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -74,6 +74,7 @@ class BuildType: ) - {"debug_cache"} _based = True +_legacy = False def flip_if_not_based(b: bool) -> bool: @@ -84,6 +85,14 @@ def flip_if_not_based(b: bool) -> bool: return b if _based else not b +def flip_if_legacy(b: bool) -> bool: + """Flips this bool if we are legacy. + + Used to run tests in old mode. + """ + return not b if _legacy else b + + # Features that are currently incomplete/experimental TYPE_VAR_TUPLE: Final = "TypeVarTuple" UNPACK: Final = "Unpack" @@ -156,6 +165,7 @@ def __init__(self) -> None: self.incomplete_is_typed = flip_if_not_based(False) self.bare_literals = True self.ignore_missing_py_typed = False + self.ide = False # disallow_any options self.disallow_any_generics = flip_if_not_based(True) @@ -208,7 +218,7 @@ def __init__(self) -> None: self.strict_optional = True # Show "note: In function "foo":" messages. - self.show_error_context = False + self.show_error_context = flip_if_legacy(True) # Use nicer output (when possible). self.color_output = True @@ -343,9 +353,9 @@ def __init__(self) -> None: self.show_column_numbers: bool = flip_if_not_based(True) self.show_error_end: bool = False self.hide_error_codes = False - self.show_error_code_links = False + self.show_error_code_links = True # 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 diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 0da795ee9f6cf..2c436d12dd1c9 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -237,7 +237,7 @@ def test_module(module_name: str) -> Iterator[Error]: runtime_desc=( "This is most likely the fault of something very dynamic in your library. " "It's also possible this is a bug in stubtest.\nIf in doubt, please " - "open an issue at https://github.com/python/mypy\n\n" + "open an issue at https://github.com/python/basedmypy\n\n" + traceback.format_exc().strip() ), ) diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 62eb94f99585a..63120607bdeed 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -336,8 +336,8 @@ def parse_options( if flags: flag_list: list[str] = safe(flags.group(1)).split() + flag_list = ["--no-pretty", "--hide-error-context", "--hide-error-code-links"] + flag_list if based: - flag_list.insert(0, "--default-return") flag_list.append("--hide-column-numbers") flag_list.extend(["--enable-error-code", "no-untyped-usage"]) else: @@ -357,6 +357,7 @@ def parse_options( raise RuntimeError("Specifying targets via the flags pragma is not supported.") if not based and "--show-error-codes" not in flag_list: options.hide_error_codes = True + print(options.pretty) else: flag_list = [] options = Options() @@ -371,6 +372,9 @@ def parse_options( options.hide_error_codes = True options.force_uppercase_builtins = True options.force_union_syntax = True + options.pretty = False + options.show_error_context = False + options.show_error_code_links = False # Allow custom python version to override testfile_pyversion. if all(flag.split("=")[0] not in ["--python-version", "-2", "--py2"] for flag in flag_list): options.python_version = testfile_pyversion(testcase.file) diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 383377ca55260..a9e54ebb0d976 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -69,6 +69,10 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None: if not based: args.append("--no-strict") args.append("--no-default-return") + if "--pretty" not in args: + args.append("--no-pretty") + if "--show-error-code-links" not in args and "--ide" not in args: + args.append("--hide-error-code-links") if "--error-summary" not in args: args.append("--no-error-summary") if "--show-error-codes" not in args and not based: @@ -85,7 +89,7 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None: env.pop("COLUMNS", None) extra_path = os.path.join(os.path.abspath(test_temp_dir), "pypath") env["PYTHONPATH"] = PREFIX - env["__MYPY_UNDER_TEST__"] = "1" + env["__MYPY_UNDER_TEST__"] = "1" if based else "2" if os.path.isdir(extra_path): env["PYTHONPATH"] += os.pathsep + extra_path cwd = os.path.join(test_temp_dir, custom_cwd or "") diff --git a/mypy/test/testdaemon.py b/mypy/test/testdaemon.py index 5bd3340e0fb6a..b3f2c8cc95fa9 100644 --- a/mypy/test/testdaemon.py +++ b/mypy/test/testdaemon.py @@ -51,7 +51,7 @@ def test_daemon(testcase: DataDrivenTestCase) -> None: if cmd.split()[1] in ("start", "restart", "run"): cmd = cmd.replace( "-- ", - "-- --no-strict --no-infer-function-types --no-default-return --hide-column-numbers ", + "-- --no-strict --no-infer-function-types --no-default-return --hide-column-numbers --no-pretty --hide-error-context ", ) sts, output = run_cmd(cmd) output_lines = output.splitlines() diff --git a/mypy/test/testerrorstream.py b/mypy/test/testerrorstream.py index e11a760ea83f9..020d97e2409d0 100644 --- a/mypy/test/testerrorstream.py +++ b/mypy/test/testerrorstream.py @@ -29,6 +29,8 @@ def test_error_stream(testcase: DataDrivenTestCase) -> None: options = Options() options.show_traceback = True options.hide_error_codes = True + options.show_error_context = False + options.pretty = False logged_messages: list[str] = [] diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index bef164634f5c4..2a6077d66019d 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -126,7 +126,9 @@ def test_pep561(testcase: DataDrivenTestCase) -> None: f.write(f"{s}\n") cmd_line.append(program) - cmd_line.extend(["--no-error-summary", "--hide-error-codes", "--no-strict"]) + cmd_line.extend( + ["--no-error-summary", "--hide-error-codes", "--no-strict", "--no-pretty", "--hide-error-context"] + ) if python_executable != sys.executable: cmd_line.append(f"--python-executable={python_executable}") diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py index b8bdc00f86340..73539789e771b 100644 --- a/mypy/test/testpythoneval.py +++ b/mypy/test/testpythoneval.py @@ -57,6 +57,8 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None "--no-infer-function-types", "--force-uppercase-builtins", "--no-strict", + "--no-pretty", + "--hide-error-context", ] interpreter = python3_path mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}") diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 751aea0298b2d..e290e3bb03c36 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -116,10 +116,12 @@ def run_stubtest( f.write(stub) with open(f"{TEST_MODULE_NAME}.py", "w") as f: f.write(runtime) - if config_file: - with open(f"{TEST_MODULE_NAME}_config.ini", "w") as f: - f.write(config_file) - options = options + ["--mypy-config-file", f"{TEST_MODULE_NAME}_config.ini"] + if not config_file: + config_file = "[mypy]" + config_file += "\nshow_error_code_links=false\npretty=false" + with open(f"{TEST_MODULE_NAME}_config.ini", "w") as f: + f.write(config_file) + options = options + ["--mypy-config-file", f"{TEST_MODULE_NAME}_config.ini"] output = io.StringIO() mypy.options._based = False with contextlib.redirect_stdout(output): diff --git a/mypyc/test/testutil.py b/mypyc/test/testutil.py index bc78fc4e3f834..dc78fd0b482e4 100644 --- a/mypyc/test/testutil.py +++ b/mypyc/test/testutil.py @@ -108,6 +108,7 @@ def build_ir_for_single_file2( mypy.options._based = False options = Options() + options.show_error_context = False options.default_return = False options.show_traceback = True options.hide_error_codes = True diff --git a/test-data/unit/check-based-misc.test b/test-data/unit/check-based-misc.test index 19fb7c3fa559c..8ac78f0b9f8ae 100644 --- a/test-data/unit/check-based-misc.test +++ b/test-data/unit/check-based-misc.test @@ -13,3 +13,11 @@ from typing import Any a: Any = 1 # E: Explicit "Any" is not allowed [no-any-explicit] reveal_type(a) # type: ignore[no-any-expr] # N: Revealed type is "Any" + + +[case testFakeColumn] +# flags: --show-error-end +if True: + 1 # type: ignore[operator] +[out] +main:3:5:3:31: error: Unused "type: ignore" comment [unused-ignore] diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 5b689ac107b6c..36cf4208f019d 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2209,7 +2209,7 @@ list(1) # E: No overload variant of "list" matches argument type "int" [call-o # N: Possible overload variants: \ # N: def [T] __init__(self) -> List[T] \ # N: def [T] __init__(self, x: Iterable[T]) -> List[T] \ - # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-call-overload for more info + # N: See https://kotlinisland.github.io/basedmypy/_refs.html#code-call-overload for more info list(2) # E: No overload variant of "list" matches argument type "int" [call-overload] \ # N: Possible overload variants: \ # N: def [T] __init__(self) -> List[T] \ diff --git a/test-data/unit/cmdline-based-baseline.test b/test-data/unit/cmdline-based-baseline.test index 25c5664de7cc4..6fa2a382e9e8c 100644 --- a/test-data/unit/cmdline-based-baseline.test +++ b/test-data/unit/cmdline-based-baseline.test @@ -328,6 +328,7 @@ def foo() -> None: [file .mypy/baseline.json] {"files": {"main.py": [{"offset": 1, "code": "misc", "message": "test", "src": ""}]}, "format": "1.7", "targets": ["file:pkg"]} [out] +pkg/main.py: note: In function "foo": pkg/main.py:3:5: note: Revealed local types are: pkg/main.py:3:5: note: a: int Success: no issues found in 1 source file @@ -575,3 +576,28 @@ def f(): ... [out] a.py:2:1: error: Unexpected keyword argument "a" for "f" [call-arg] b.py:4:1: note: "f" defined here + + +[case testErrorLink] +# cmd: mypy a.py --show-error-code-links +[file a.py] +a +[file .mypy/baseline.json] +{ + "files": { + "a.py": [ + { + "code": "name-defined", + "column": 0, + "message": "Name \"a\" is not defined", + "offset": 1, + "src": "a", + "target": "a" + } + ] + }, + "format": "1.7", + "targets": [ + "file:a.py" + ] +} diff --git a/test-data/unit/cmdline-based.test b/test-data/unit/cmdline-based.test index 2ddaff9082890..437fa0ea59ed0 100644 --- a/test-data/unit/cmdline-based.test +++ b/test-data/unit/cmdline-based.test @@ -41,3 +41,15 @@ x = 0 [out] main.py:2:13: note: Revealed type is "int" == Return code: 0 + + +[case testIdeCodes] +# cmd: mypy --ide main.py +[file main.py] +1 + "" +1 + "" +[out] +main.py:1:5:1:6: error: Unsupported operand types for + ("int" and "str") [operator] +main.py:1:5:1:6: note: See https://kotlinisland.github.io/basedmypy/_refs.html#code-operator for more info +main.py:2:5:2:6: error: Unsupported operand types for + ("int" and "str") [operator] +main.py:2:5:2:6: note: See https://kotlinisland.github.io/basedmypy/_refs.html#code-operator for more info