From 8dca0aef4d90494d5296e74d9f889c34dd9dd371 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Dec 2019 15:24:19 +0000 Subject: [PATCH 1/4] Support silencing only some error codes --- mypy/build.py | 11 ++++++--- mypy/config_parser.py | 1 + mypy/errors.py | 24 +++++++++++++++--- mypy/main.py | 10 ++++++++ mypy/options.py | 4 +++ mypy/server/update.py | 5 ++-- test-data/unit/check-flags.test | 33 +++++++++++++++++++++++++ test-data/unit/check-inline-config.test | 9 +++++++ 8 files changed, 88 insertions(+), 9 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 15be2adf5611..f3dc2f57eed1 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -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. @@ -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]]: @@ -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 diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 6a94757f58ed..becce1183b21 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -82,6 +82,7 @@ def split_and_match_files(paths: str) -> List[str]: '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(',')], + 'ignore_error_codes': lambda s: [p.strip() for p in s.split(',')], 'package_root': lambda s: [p.strip() for p in s.split(',')], 'cache_dir': expand_path, 'python_executable': expand_path, diff --git a/mypy/errors.py b/mypy/errors.py index 5c37365160c1..078f8ef9cd74 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -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 @@ -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]] @@ -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() @@ -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) @@ -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 diff --git a/mypy/main.py b/mypy/main.py index 61f069a79950..6256029c485a 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -441,6 +441,9 @@ def add_invertible_flag(flag: str, add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True, help="Warn about unused '[mypy-]' config sections", group=config_group) + config_group.add_argument( + '--ignore-error-codes', metavar='NAME', action='append', default=[], + help="Ignore errors with this error code (may be repeated)") imports_group = parser.add_argument_group( title='Import discovery', @@ -857,6 +860,13 @@ def add_invertible_flag(flag: str, parser.error("You can't make a variable always true and always false (%s)" % ', '.join(sorted(overlap))) + # Interpret --ignore-error-codes=attr-defined,arg-type as two separate error codes. + if options.ignore_error_codes: + split_codes = [] + for code in options.ignore_error_codes: + split_codes.extend([c.strip() for c in code.split(',')]) + options.ignore_error_codes = split_codes + # Set build flags. if options.strict_optional_whitelist is not None: # TODO: Deprecate, then kill this flag diff --git a/mypy/options.py b/mypy/options.py index 38072b821d15..6d474bdc02cd 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -43,6 +43,7 @@ class BuildType: "mypyc", "no_implicit_optional", "implicit_reexport", + "ignore_error_codes", "show_none_errors", "strict_optional", "strict_optional_whitelist", @@ -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. + self.ignore_error_codes = [] # type: List[str] + # Apply strict None checking self.strict_optional = True diff --git a/mypy/server/update.py b/mypy/server/update.py index 2e256e9d7f3c..62fac416d1b9 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -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: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index a2db76dd9434..4ab23bdddbd2 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -1232,3 +1232,36 @@ 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 --ignore-error-codes=name-defined +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 +[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") diff --git a/test-data/unit/check-inline-config.test b/test-data/unit/check-inline-config.test index 4cf82b03e671..4a4501df1ec1 100644 --- a/test-data/unit/check-inline-config.test +++ b/test-data/unit/check-inline-config.test @@ -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: ... +test(0) From 981457c097b5c6955a241254c6b15e031db97c19 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Dec 2019 17:53:08 +0000 Subject: [PATCH 2/4] Some reshuffle; add docs --- docs/source/command_line.rst | 35 +++++++++++++++++++++++++++++++++ docs/source/config_file.rst | 4 ++++ docs/source/error_codes.rst | 4 ++-- docs/source/inline_config.rst | 4 +++- mypy/config_parser.py | 11 ++++++----- mypy/main.py | 14 ++++--------- mypy/util.py | 5 +++++ test-data/unit/check-flags.test | 5 +++-- 8 files changed, 62 insertions(+), 20 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 0cfcbe98cb86..fd9c3870e860 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -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 + error codes as a comma separated list (there should be no spaces after commas). + 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 + 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 ` and + :ref:`optional error codes `. + .. _incremental: diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index d4ff74258745..a101db488f80 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -439,6 +439,10 @@ 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. + See :option:`--ignore-error-codes` for more details. + Incremental mode **************** diff --git a/docs/source/error_codes.rst b/docs/source/error_codes.rst index 869d17842b7a..0e1c67e23222 100644 --- a/docs/source/error_codes.rst +++ b/docs/source/error_codes.rst @@ -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. Alternatively use :option:`--ignore-error-codes` for more +coarse-grained (per file or per directory) control. .. note:: diff --git a/docs/source/inline_config.rst b/docs/source/inline_config.rst index a587983cc74a..b583f34447e6 100644 --- a/docs/source/inline_config.rst +++ b/docs/source/inline_config.rst @@ -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) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index becce1183b21..30ade6586ae7 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -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]: @@ -79,11 +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(',')], - 'ignore_error_codes': 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 diff --git a/mypy/main.py b/mypy/main.py index 6256029c485a..39e8332b976d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -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__ @@ -441,9 +442,6 @@ def add_invertible_flag(flag: str, add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True, help="Warn about unused '[mypy-]' config sections", group=config_group) - config_group.add_argument( - '--ignore-error-codes', metavar='NAME', action='append', default=[], - help="Ignore errors with this error code (may be repeated)") imports_group = parser.add_argument_group( title='Import discovery', @@ -643,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)") incremental_group = parser.add_argument_group( title='Incremental mode', @@ -860,13 +861,6 @@ def add_invertible_flag(flag: str, parser.error("You can't make a variable always true and always false (%s)" % ', '.join(sorted(overlap))) - # Interpret --ignore-error-codes=attr-defined,arg-type as two separate error codes. - if options.ignore_error_codes: - split_codes = [] - for code in options.ignore_error_codes: - split_codes.extend([c.strip() for c in code.split(',')]) - options.ignore_error_codes = split_codes - # Set build flags. if options.strict_optional_whitelist is not None: # TODO: Deprecate, then kill this flag diff --git a/mypy/util.py b/mypy/util.py index 8f9448bcff4c..457b3a776fa6 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -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 = '' diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 4ab23bdddbd2..8db02e75ebff 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -1234,8 +1234,9 @@ 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 --ignore-error-codes=name-defined -not_defined +# 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: ... From 44806eff5f9d8223ad85fd38e6418e57b3c258fa Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 8 Dec 2019 00:32:45 +0000 Subject: [PATCH 3/4] Make indentation consistent; fix option ref --- docs/source/command_line.rst | 48 ++++++++++++++++++------------------ docs/source/config_file.rst | 3 ++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index fd9c3870e860..e38b949484f6 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -595,38 +595,38 @@ in error messages. .. option:: --ignore-error-codes CODES - This flag makes mypy ignore all errors with given error codes. Flag accepts - error codes as a comma separated list (there should be no spaces after commas). - For example, by default mypy would produce the following errors: + This flag makes mypy ignore all errors with given error codes. Flag accepts + error codes as a comma separated list (there should be no spaces after commas). + For example, by default mypy would produce the following errors: - .. code-block:: python + .. code-block:: python - class Dynamic: - def __init__(self, attr: str, value: object) -> None: - setattr(self, attr, value) + class Dynamic: + def __init__(self, attr: str, value: object) -> None: + setattr(self, attr, value) - magic_builtin # error: Name "magic_builtin" is not defined - 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") + magic_builtin # error: Name "magic_builtin" is not defined + 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: + But when used as ``mypy --ignore-error-codes=attr-defined,name-defined test.py`` + it will produce the following errors: - .. code-block:: python + .. code-block:: python - class Dynamic: - def __init__(self, attr: str, value: object) -> None: - setattr(self, attr, value) + 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") + 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 ` and - :ref:`optional error codes `. + To make mypy show error codes in error messages use :option:`--show-error-codes`. + See also the lists of :ref:`default error codes ` and + :ref:`optional error codes `. .. _incremental: diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index a101db488f80..e3745f27f065 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -441,7 +441,8 @@ These options may only be set in the global section (``[mypy]``). ``ignore_error_codes`` (comma-separated list of strings) Ignore errors with these error codes in given files or directories. - See :option:`--ignore-error-codes` for more details. + See :option:`--ignore-error-codes ` for + more details. Incremental mode From 5220a2057ff04ca15a4c42c6588caa0178e517c6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 8 Dec 2019 01:03:02 +0000 Subject: [PATCH 4/4] One more doc fix --- docs/source/error_codes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/error_codes.rst b/docs/source/error_codes.rst index 0e1c67e23222..3f8d0cd111c3 100644 --- a/docs/source/error_codes.rst +++ b/docs/source/error_codes.rst @@ -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. Alternatively use :option:`--ignore-error-codes` for more -coarse-grained (per file or per directory) control. +error codes. Also you can use :option:`--ignore-error-codes ` +for more coarse-grained (per file or per directory) silencing of errors. .. note::