Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support silencing only some error codes #8102

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,41 @@ in error messages.

Show absolute paths to files.

.. option:: --ignore-error-codes CODES

This flag makes mypy ignore all errors with given error codes. Flag accepts
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that we should include a link to documentatino about error codes here instead of later. The text "error codes" could link to the relevant topic.

Grammar nit: "Flag" -> "This flag"

error codes as a comma separated list (there should be no spaces after commas).
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be better for usability if spaces around commas were ignored. Error codes can't have spaces.

Grammar nit: "comma separated" -> "comma-separated" (we use the latter spelling elsewhere)

For example, by default mypy would produce the following errors:

.. code-block:: python

class Dynamic:
def __init__(self, attr: str, value: object) -> None:
setattr(self, attr, value)

magic_builtin # error: Name "magic_builtin" is not defined
Copy link
Collaborator

Choose a reason for hiding this comment

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

The comments make this example look kind of busy, since some of the messages are long. Also, it would be nice to include the error codes in the message, as now it's unclear where the error codes come from (--show-error-codes is introduced only later).

Here's a suggested change:

  • Remove the comments from the example.
  • Include full output from a mypy run, without --ignore-error-codes flag, but with --show-error-codes.
  • Show output from another mypy run, now with the the various error codes ignored.

Dynamic("test", 0).test # error: "Dynamic" has no attribute "test"
x: int = "no" # error: Incompatible types in assignment
# (expression has type "str", variable has type "int")

But when used as ``mypy --ignore-error-codes=attr-defined,name-defined test.py``
it will produce the following errors:

.. code-block:: python

class Dynamic:
def __init__(self, attr: str, value: object) -> None:
setattr(self, attr, value)

magic_builtin # No error
Dynamic("test", 0).test # No error
x: int = "no" # error: Incompatible types in assignment
# (expression has type "str", variable has type "int")

To make mypy show error codes in error messages use :option:`--show-error-codes`.
See also the lists of :ref:`default error codes <error-code-list>` and
:ref:`optional error codes <error-codes-optional>`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that we should have a warning here, as ignoring some errors could be quite confusing and dangerous, especially misc.



.. _incremental:

Expand Down
5 changes: 5 additions & 0 deletions docs/source/config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ These options may only be set in the global section (``[mypy]``).
``show_absolute_path`` (bool, default False)
Show absolute paths to files.

``ignore_error_codes`` (comma-separated list of strings)
Ignore errors with these error codes in given files or directories.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The part "in given files or directories" seems redundant. It's not included in other descriptions, and so it raises the question why this option applies to given files or directories, while other options don't apply to given files or directories?

See :option:`--ignore-error-codes <mypy --ignore-error-codes>` for
more details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Really tiny consistency nit: we use "for more information" about and "more details here". Maybe use "for more information" here as well.



Incremental mode
****************
Expand Down
4 changes: 2 additions & 2 deletions docs/source/error_codes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Silencing errors based on error codes
You can use a special comment ``# type: ignore[code, ...]`` to only
ignore errors with a specific error code (or codes) on a particular
line. This can be used even if you have not configured mypy to show
error codes. Currently it's only possible to disable arbitrary error
codes on individual lines using this comment.
error codes. Also you can use :option:`--ignore-error-codes <mypy --ignore-error-codes>`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style nit: "Also" seems redundant.

for more coarse-grained (per file or per directory) silencing of errors.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The actual command line flag only allows global silencing of errors, but this implies that it's per file or per directory. Either leave the parenthetical remark out, or somehow make it clear that per-file/directory options are only available through the config file or # mypy: ... comments. For consistency, I'd leave it out, since we probably don't want to discuss this separately for every reference to every option.


.. note::

Expand Down
4 changes: 3 additions & 1 deletion docs/source/inline_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ Values are specified using ``=``, but ``= True`` may be omitted:

Multiple flags can be separated by commas or placed on separate
lines. To include a comma as part of an option's value, place the
value inside quotes:
value inside quotes. Quotes are still needed if there is a single
option on a line:

.. code-block:: python

# mypy: disallow-untyped-defs, always-false="FOO,BAR"
# mypy: ignore-error-codes="override,attr-defined"

Like in the configuration file, options that take a boolean value may be
inverted by adding ``no-`` to their name or by (when applicable)
Expand Down
11 changes: 8 additions & 3 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,8 @@ def is_module(self, id: str) -> bool:
"""Is there a file in the file system corresponding to module id?"""
return find_module_simple(id, self) is not None

def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> MypyFile:
def parse_file(self, id: str, path: str, source: str,
ignore_errors: bool, ignore_error_codes: Iterable[str]) -> MypyFile:
"""Parse the source of a file with the given name.

Raise CompileError if there is a parse error.
Expand All @@ -762,7 +763,10 @@ def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> My
self.log("Bailing due to parse errors")
self.errors.raise_error()

self.errors.set_file_ignored_lines(path, tree.ignored_lines, ignore_errors)
self.errors.set_file_ignored_lines_and_codes(path,
tree.ignored_lines,
ignore_error_codes,
ignore_errors)
return tree

def load_fine_grained_deps(self, id: str) -> Dict[str, Set[str]]:
Expand Down Expand Up @@ -2015,7 +2019,8 @@ def parse_file(self) -> None:

self.parse_inline_configuration(source)
self.tree = manager.parse_file(self.id, self.xpath, source,
self.ignore_all or self.options.ignore_errors)
self.ignore_all or self.options.ignore_errors,
self.options.ignore_error_codes)

modules[self.id] = self.tree

Expand Down
10 changes: 6 additions & 4 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from mypy import defaults
from mypy.options import Options, PER_MODULE_OPTIONS
from mypy.util import split_commas


def parse_version(v: str) -> Tuple[int, int]:
Expand Down Expand Up @@ -79,10 +80,11 @@ def split_and_match_files(paths: str) -> List[str]:
# These two are for backwards compatibility
'silent_imports': bool,
'almost_silent': bool,
'plugins': lambda s: [p.strip() for p in s.split(',')],
'always_true': lambda s: [p.strip() for p in s.split(',')],
'always_false': lambda s: [p.strip() for p in s.split(',')],
'package_root': lambda s: [p.strip() for p in s.split(',')],
'plugins': split_commas,
'always_true': split_commas,
'always_false': split_commas,
'ignore_error_codes': split_commas,
'package_root': split_commas,
'cache_dir': expand_path,
'python_executable': expand_path,
} # type: Final
Expand Down
24 changes: 20 additions & 4 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import traceback
from collections import OrderedDict, defaultdict

from typing import Tuple, List, TypeVar, Set, Dict, Optional, TextIO, Callable
from typing import Tuple, List, TypeVar, Set, Dict, Optional, TextIO, Callable, Iterable
from typing_extensions import Final

from mypy.scope import Scope
Expand Down Expand Up @@ -133,6 +133,9 @@ class Errors:
# (path -> line -> error-codes)
ignored_lines = None # type: Dict[str, Dict[int, List[str]]]

# Error codes ignored for a given file.
ignored_codes = None # type: Dict[str, Set[str]]

# Lines on which an error was actually ignored.
used_ignored_lines = None # type: Dict[str, Set[int]]

Expand Down Expand Up @@ -179,6 +182,7 @@ def initialize(self) -> None:
self.import_ctx = []
self.function_or_member = [None]
self.ignored_lines = OrderedDict()
self.ignored_codes = OrderedDict()
self.used_ignored_lines = defaultdict(set)
self.ignored_files = set()
self.only_once_messages = set()
Expand Down Expand Up @@ -234,10 +238,19 @@ def set_file(self, file: str,
self.target_module = module
self.scope = scope

def set_file_ignored_lines(self, file: str,
ignored_lines: Dict[int, List[str]],
ignore_all: bool = False) -> None:
def set_file_ignored_lines_and_codes(self, file: str,
ignored_lines: Dict[int, List[str]],
ignored_codes: Iterable[str],
ignore_all: bool = False) -> None:
"""Set information about which errors should be ignored in a given file.

Args:
ignored_lines: Mapping from line number to error codes to ignore on this line
ignored_codes: Error codes to ignore globally in this file
ignore_all: Ignore all errors in this file
"""
self.ignored_lines[file] = ignored_lines
self.ignored_codes[file] = set(ignored_codes)
if ignore_all:
self.ignored_files.add(file)

Expand Down Expand Up @@ -343,6 +356,9 @@ def add_error_info(self, info: ErrorInfo) -> None:
return
if file in self.ignored_files:
return
if (info.code and file in self.ignored_codes and
info.code.code in self.ignored_codes[file]):
return
if info.only_once:
if info.message in self.only_once_messages:
return
Expand Down
4 changes: 4 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from mypy.options import Options, BuildType
from mypy.config_parser import parse_version, parse_config_file
from mypy.split_namespace import SplitNamespace
from mypy.util import split_commas

from mypy.version import __version__

Expand Down Expand Up @@ -640,6 +641,9 @@ def add_invertible_flag(flag: str,
add_invertible_flag('--show-absolute-path', default=False,
help="Show absolute paths to files",
group=error_group)
error_group.add_argument(
'--ignore-error-codes', metavar='CODES', type=split_commas,
help="Ignore errors with these error codes (comma separated)")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Spelling nit: use "comma-separated" (similar to above)


incremental_group = parser.add_argument_group(
title='Incremental mode',
Expand Down
4 changes: 4 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class BuildType:
"mypyc",
"no_implicit_optional",
"implicit_reexport",
"ignore_error_codes",
"show_none_errors",
"strict_optional",
"strict_optional_whitelist",
Expand Down Expand Up @@ -132,6 +133,9 @@ def __init__(self) -> None:
# Files in which to ignore all non-fatal errors
self.ignore_errors = False

# Ignore errors with these error codes in a given file.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: "in a given files." seems redunant. Also omit period at the end for consistency?

self.ignore_error_codes = [] # type: List[str]

# Apply strict None checking
self.strict_optional = True

Expand Down
5 changes: 3 additions & 2 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,8 +902,9 @@ def key(node: FineGrainedDeferredNode) -> int:
nodes = sorted(nodeset, key=key)

options = graph[module_id].options
manager.errors.set_file_ignored_lines(
file_node.path, file_node.ignored_lines, options.ignore_errors)
manager.errors.set_file_ignored_lines_and_codes(
file_node.path, file_node.ignored_lines,
options.ignore_error_codes, options.ignore_errors)

targets = set()
for node in nodes:
Expand Down
5 changes: 5 additions & 0 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@ def count_stats(errors: List[str]) -> Tuple[int, int]:
return len(errors), len(files)


def split_commas(text: str) -> List[str]:
"""Split a comma separated list."""
return [c.strip() for c in text.split(',')]


def split_words(msg: str) -> List[str]:
"""Split line of text into words (but not within quoted groups)."""
next_word = ''
Expand Down
34 changes: 34 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -1232,3 +1232,37 @@ A = List # OK
B = List[A] # E:10: Missing type parameters for generic type "A"
x: A # E:4: Missing type parameters for generic type "A"
[builtins fixtures/list.pyi]

[case testIgnoreErrorCodesGlobal]
# flags: --ignore-error-codes=attr-defined,arg-type

not_defined # E: Name 'not_defined' is not defined
x: int = 'no' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
'no'.no_way
def test(x: str) -> None: ...
test(0)

[case testIgnoreErrorCodesPerFile]
# flags: --config-file tmp/mypy.ini
import b
not_defined
x: int = 'no'
'no'.no_way
def test(x: str) -> None: ...
test(0)
[file b.py]
not_defined
x: int = 'no'
'no'.no_way
def test(x: str) -> None: ...
test(0)
[file mypy.ini]
\[mypy]
ignore_error_codes = attr-defined, arg-type
\[mypy-b]
ignore_error_codes = name-defined, assignment
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to eventually move various strictness flags to be enabled/disabled through error codes. To do this, it seems that per-file options need to be a delta over global settings, instead of replacing them. Also, command-line options probably should be a delta over settings in the ini file. I.e., the ignored error codes would be the union of error codes ignored in the config file, on the command line, and in per-file options.

The logic gets a bit more involved once we allow enabling error codes as well, but at least we should be able to design the semantic for ignoring error codes in a way that we don't need to introduce a compatibility break.

[out]
tmp/b.py:3: error: "str" has no attribute "no_way"
tmp/b.py:5: error: Argument 1 to "test" has incompatible type "int"; expected "str"
main:3: error: Name 'not_defined' is not defined
main:4: error: Incompatible types in assignment (expression has type "str", variable has type "int")
9 changes: 9 additions & 0 deletions test-data/unit/check-inline-config.test
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,12 @@ main:4: error: Unterminated quote in configuration comment
# mypy: skip-file
[out]
main:1: error: Unrecognized option: skip_file = True

[case testIgnoreErrorCodesInline]
# mypy: ignore-error-codes="attr-defined,arg-type"

not_defined # E: Name 'not_defined' is not defined
x: int = 'no' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
'no'.no_way
def test(x: str) -> None: ...
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also add daemon test case (to make sure the daemon applies these after updates).

test(0)