diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index d4ff74258745..22ec2ec9a029 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -376,6 +376,8 @@ no analog available via the command line options. ``ignore_errors`` (bool, default False) Ignores all non-fatal errors. +``ignore_errors_by_regex`` (list of regex-pattern, with each pattern on a new line) + Ignores all errors, which match one of the given regex-pattern. Miscellaneous strictness flags ****************************** diff --git a/mypy/build.py b/mypy/build.py index 15be2adf5611..4a38bdf604e2 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -209,7 +209,8 @@ def _build(sources: List[BuildSource], options.show_error_codes, options.pretty, lambda path: read_py_file(path, cached_read, options.python_version), - options.show_absolute_path) + options.show_absolute_path, + options.ignore_errors_by_regex) plugin, snapshot = load_plugins(options, errors, stdout, extra_plugins) # Construct a build manager object to hold state during the build. diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 6a94757f58ed..b3160cbb3e54 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -83,6 +83,7 @@ def split_and_match_files(paths: str) -> List[str]: '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(',')], + 'ignore_errors_by_regex': lambda s: [p.strip() for p in s.split('\n') if p != ''], 'cache_dir': expand_path, 'python_executable': expand_path, } # type: Final diff --git a/mypy/errors.py b/mypy/errors.py index 5c37365160c1..1bbe32cbaa07 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -1,6 +1,7 @@ import os.path import sys import traceback +import re from collections import OrderedDict, defaultdict from typing import Tuple, List, TypeVar, Set, Dict, Optional, TextIO, Callable @@ -163,7 +164,8 @@ def __init__(self, show_error_codes: bool = False, pretty: bool = False, read_source: Optional[Callable[[str], Optional[List[str]]]] = None, - show_absolute_path: bool = False) -> None: + show_absolute_path: bool = False, + ignore_errors_by_regex: Optional[List[str]] = None) -> None: self.show_error_context = show_error_context self.show_column_numbers = show_column_numbers self.show_error_codes = show_error_codes @@ -171,6 +173,11 @@ def __init__(self, self.pretty = pretty # We use fscache to read source code when showing snippets. self.read_source = read_source + self.ignore_errors_by_regex = ignore_errors_by_regex + if ignore_errors_by_regex is not None: + self.ignore_message_regex_patterns = [re.compile(p) for p in ignore_errors_by_regex] + else: + self.ignore_message_regex_patterns = [] self.initialize() def initialize(self) -> None: @@ -194,7 +201,8 @@ def copy(self) -> 'Errors': self.show_error_codes, self.pretty, self.read_source, - self.show_absolute_path) + self.show_absolute_path, + self.ignore_errors_by_regex) new.file = self.file new.import_ctx = self.import_ctx[:] new.function_or_member = self.function_or_member[:] @@ -349,8 +357,25 @@ def add_error_info(self, info: ErrorInfo) -> None: self.only_once_messages.add(info.message) self._add_error_info(file, info) + def is_ignore_message_by_regex(self, message: str) -> bool: + for p in self.ignore_message_regex_patterns: + if p.match(message): + return True + + return False + + def is_line_already_ignored(self, file: str, line: int) -> bool: + if file in self.used_ignored_lines: + if line in self.used_ignored_lines[file]: + return True + return False + def is_ignored_error(self, line: int, info: ErrorInfo, ignores: Dict[int, List[str]]) -> bool: - if line not in ignores: + if info.severity == 'note' and self.is_line_already_ignored(info.origin[0], line): + return True + elif self.is_ignore_message_by_regex(info.message): + return True + elif line not in ignores: return False elif not ignores[line]: # Empty list means that we ignore all errors diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 812defbc1452..bd31a1b686db 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -142,11 +142,13 @@ def parse(source: Union[str, bytes], on failure. Otherwise, use the errors object to report parse errors. """ raise_on_error = False - if errors is None: - errors = Errors() - raise_on_error = True if options is None: options = Options() + if errors is None: + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) + raise_on_error = True errors.set_file(fnam, module) is_stub_file = fnam.endswith('.pyi') try: diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index f34319a67b2d..a4df10bd3fc9 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -99,11 +99,13 @@ def parse(source: Union[str, bytes], on failure. Otherwise, use the errors object to report parse errors. """ raise_on_error = False - if errors is None: - errors = Errors() - raise_on_error = True if options is None: options = Options() + if errors is None: + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) + raise_on_error = True errors.set_file(fnam, module) is_stub_file = fnam.endswith('.pyi') try: diff --git a/mypy/options.py b/mypy/options.py index 38072b821d15..d47c9fea795d 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -38,6 +38,7 @@ class BuildType: "follow_imports", "follow_imports_for_stubs", "ignore_errors", + "ignore_errors_by_regex", "ignore_missing_imports", "local_partial_types", "mypyc", @@ -132,6 +133,9 @@ def __init__(self) -> None: # Files in which to ignore all non-fatal errors self.ignore_errors = False + # RegEx patterns of errors, which should be ignored + self.ignore_errors_by_regex = [] # type: List[str] + # Apply strict None checking self.strict_optional = True diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 214dc7763819..e432eb4002c6 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1321,7 +1321,9 @@ def parse_source_file(mod: StubSource, mypy_options: MypyOptions) -> None: with open(mod.path, 'rb') as f: data = f.read() source = mypy.util.decode_python_encoding(data, mypy_options.python_version) - errors = Errors() + errors = Errors( + ignore_errors_by_regex=mypy_options.ignore_errors_by_regex + ) mod.ast = mypy.parse.parse(source, fnam=mod.path, module=mod.module, errors=errors, options=mypy_options) mod.ast._fullname = mod.module diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 2747d1c034d1..3dde5812641f 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -50,6 +50,7 @@ 'check-typeddict.test', 'check-type-aliases.test', 'check-ignore.test', + 'check-ignore-by-regex.test', 'check-type-promotion.test', 'check-semanal-error.test', 'check-flags.test', diff --git a/mypy/test/testgraph.py b/mypy/test/testgraph.py index 3a6a8f70899a..42e17a57bffa 100644 --- a/mypy/test/testgraph.py +++ b/mypy/test/testgraph.py @@ -39,8 +39,10 @@ def test_scc(self) -> None: frozenset({'D'})}) def _make_manager(self) -> BuildManager: - errors = Errors() options = Options() + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) fscache = FileSystemCache() search_paths = SearchPaths((), (), (), ()) manager = BuildManager( diff --git a/mypyc/build.py b/mypyc/build.py index 20efdce2d37b..e2512b6968c8 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -196,7 +196,9 @@ def generate_c(sources: List[BuildSource], print("Parsed and typechecked in {:.3f}s".format(t1 - t0)) if not messages and result: - errors = Errors() + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) modules, ctext = emitmodule.compile_modules_to_c( result, compiler_options=compiler_options, errors=errors, groups=groups) diff --git a/mypyc/errors.py b/mypyc/errors.py index aac543d10ee4..96be122fcbd1 100644 --- a/mypyc/errors.py +++ b/mypyc/errors.py @@ -1,13 +1,18 @@ -from typing import List +from typing import List, Optional import mypy.errors class Errors: - def __init__(self) -> None: + def __init__( + self, + ignore_errors_by_regex: Optional[List[str]] = None + ) -> None: self.num_errors = 0 self.num_warnings = 0 - self._errors = mypy.errors.Errors() + self._errors = mypy.errors.Errors( + ignore_errors_by_regex=ignore_errors_by_regex + ) def error(self, msg: str, path: str, line: int) -> None: self._errors.report(line, None, msg, severity='error', file=path) diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index a0d4812296ae..bf38ef3d9c9e 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -209,7 +209,9 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) -> compiler_options=compiler_options, groups=groups, alt_lib_path='.') - errors = Errors() + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) ir, cfiles = emitmodule.compile_modules_to_c( result, compiler_options=compiler_options, diff --git a/mypyc/test/testutil.py b/mypyc/test/testutil.py index 4bc5cbe6a04d..a15ee4b7d321 100644 --- a/mypyc/test/testutil.py +++ b/mypyc/test/testutil.py @@ -104,7 +104,9 @@ def build_ir_for_single_file(input_lines: List[str], if result.errors: raise CompileError(result.errors) - errors = Errors() + errors = Errors( + ignore_errors_by_regex=options.ignore_errors_by_regex + ) modules = genops.build_ir( [result.files['__main__']], result.graph, result.types, genops.Mapper({'__main__': None}), diff --git a/test-data/unit/check-ignore-by-regex.test b/test-data/unit/check-ignore-by-regex.test new file mode 100644 index 000000000000..5c5f84df91bf --- /dev/null +++ b/test-data/unit/check-ignore-by-regex.test @@ -0,0 +1,400 @@ +[case testIgnoreByRegExType] +# flags: --config-file tmp/mypy.ini +x = 1 +z = 'hello' +x() +z() # E: "str" not callable + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = "int" not callable + + +[case testIgnoreByRegExUndefinedName] +# flags: --config-file tmp/mypy.ini +x = 1 +y +z # E: Name 'z' is not defined + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Name 'y' is not defined + + +[case testIgnoreByRegExImportError] +# flags: --config-file tmp/mypy.ini +import xyz_m +xyz_m.foo +1() # E: "int" not callable + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation .+ 'xyz_m' + + +[case testIgnoreByRegExImportFromError] +# flags: --config-file tmp/mypy.ini +from xyz_m import a, b +a.foo +b() +1() # E: "int" not callable +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation .+ 'xyz_m' + + +[case testIgnoreByRegExImportFromErrorMultiline] +# flags: --config-file tmp/mypy.ini +from xyz_m import ( + a, b +) +a.foo +b() +1() # E: "int" not callable + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '.+' + + +[case testIgnoreByRegExImportAllError] +# flags: --config-file tmp/mypy.ini +from xyz_m import * +x # E: Name 'x' is not defined +1() # E: "int" not callable + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = .+ library .+ 'xyz_m' + + +[case testIgnoreByRegExAppliesOnlyToMissing] +# flags: --config-file tmp/mypy.ini +import a +import b +reveal_type(a.foo) # N: Revealed type is 'Any' +reveal_type(b.foo) # N: Revealed type is 'builtins.int' +a.bar() +b.bar() # E: Module has no attribute "bar" + +[file b.py] +foo = 3 + +[builtins fixtures/module_all.pyi] +[out] + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = .+ library .+ 'a' + + +[case testIgnoreByRegExAssignmentTypeError] +# flags: --config-file tmp/mypy.ini +x = 1 +y = 'mypy' +if int(): + y = 42 +if int(): + x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Incompatible types in assignment \(expression has type "int", variable has type "[^"]+"\) + + +[case testIgnoreByRegExInvalidOverride] +# flags: --config-file tmp/mypy.ini +class A: + def f(self) -> int: pass +class B(A): + def f(self) -> str: pass +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Return type "[^"]+" of "f" incompatible with return type "[^"]+" in supertype "A" + + +[case testIgnoreByRegExMissingModuleAttribute] +# flags: --config-file tmp/mypy.ini +import m +m.x = object +m.f() +m.y # E: Module has no attribute "y" +[file m.py] +[builtins fixtures/module.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Module has no attribute "[xf]" + + +[case testIgnoreByRegExTypeInferenceError] +# flags: --config-file tmp/mypy.ini +x = [] +y = x +x.append(1) +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Need type annotation for 'x' + + +[case testIgnoreByRegExTypeInferenceError2] +# flags: --config-file tmp/mypy.ini +def f() -> None: pass +x = f() +y = x +x = 1 +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = "f" does not return a value + + +[case testIgnoreByRegExTypeInferenceErrorAndMultipleAssignment1] +-- two errors on the same line: only one of them should be ignored +# flags: --config-file tmp/mypy.ini +x, y = [], [] +z = x +z = y +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Need type annotation for 'x' \(hint: "x: List\[\] = ..."\) +[out] +main:2: error: Need type annotation for 'y' (hint: "y: List[] = ...") + + +[case testIgnoreByRegExTypeInferenceErrorAndMultipleAssignment2] +-- two errors on the same line: only one of them should be ignored +# flags: --config-file tmp/mypy.ini +x, y = [], [] +z = x +z = y +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Need type annotation for 'y' \(hint: "y: List\[\] = ..."\) +[out] +main:2: error: Need type annotation for 'x' (hint: "x: List[] = ...") + + +[case testIgnoreByRegExSomeStarImportErrors] +# flags: --config-file tmp/mypy.ini +from m1 import * +from m2 import * +# We should still import things that don't conflict. +y() # E: "str" not callable +z() # E: "int" not callable +x() # E: "int" not callable +[file m1.py] +x = 1 +y = '' +[file m2.py] +x = '' +z = 1 +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Incompatible import of "x" \(imported name has type "str", local name has type "int"\) + + +[case testIgnoreByRegExdModuleDefinesBaseClass1] +# flags: --config-file tmp/mypy.ini +from m import B + +class C(B): + def f(self) -> None: + self.f(1) # E: Too many arguments for "f" of "C" + self.g(1) + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '[a-zA-Z_]+' + +[out] + + +[case testIgnoreByRegExdModuleDefinesBaseClass2] +# flags: --config-file tmp/mypy.ini +import m + +class C(m.B): + def f(self) -> None: ... + +c = C() +c.f(1) # E: Too many arguments for "f" of "C" +c.g(1) +c.x = 1 +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '[a-zA-Z_]+' + +[out] + + +[case testIgnoreByRegExdModuleDefinesBaseClassAndClassAttribute] +# flags: --config-file tmp/mypy.ini +import m + +class C(m.B): + @staticmethod + def f() -> None: pass + +C.f(1) # E: Too many arguments for "f" of "C" +C.g(1) +C.x = 1 +[builtins fixtures/staticmethod.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '[a-zA-Z_]+' + +[out] + + +[case testIgnoreByRegExdModuleDefinesBaseClassWithInheritance1] +# flags: --config-file tmp/mypy.ini +from m import B + +class C: pass +class D(C, B): + def f(self) -> None: + self.f(1) # E: Too many arguments for "f" of "D" + self.g(1) + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '[a-zA-Z_]+' + +[out] + + +[case testIgnoreByRegExdModuleDefinesBaseClassWithInheritance2] +# flags: --config-file tmp/mypy.ini +from m import B + +class C(B): pass +class D(C): + def f(self) -> None: + self.f(1) # E: Too many arguments for "f" of "D" + self.g(1) + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named '[a-zA-Z_]+' + +[out] + + +[case testIgnoreByRegExTooManyTypeArguments] +# flags: --config-file tmp/mypy.ini +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') + +class Base(Generic[T, U]): + pass + +class PartialBase(Base[T, int], Generic[T]): + pass + +class Child(PartialBase[str, int]): + pass + + +def foo(x: Base[str, int]) -> None: pass +foo(Child()) + +def bar(x: Base[str, str]) -> None: pass +bar(Child()) + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = "PartialBase" expects 1 type argument, but [0-9]+ given + +[out] +main:20: error: Argument 1 to "bar" has incompatible type "Child"; expected "Base[str, str]" + + +[case testIgnoreByRegExLineNumberWithinFile] +# flags: --config-file tmp/mypy.ini +import m +pass +m.f(kw=1) +[file m.py] +pass +def f() -> None: pass +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = ignore any error, but not this +[out] +main:4: error: Unexpected keyword argument "kw" for "f" +tmp/m.py:2: note: "f" defined here + + +[case testIgnoreByRegExUnexpectedKeywordArgument] +# flags: --config-file tmp/mypy.ini +import m +m.f(kw=1) # type: ignore +[file m.py] +def f() -> None: pass +[out] + +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Unexpected keyword argument ".+" for "f" + + +[case testIgnoreByRegExButNotBlockingError] +# flags: --config-file tmp/mypy.ini +yield # E: 'yield' outside function +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = 'yield' outside function + + +[case testIgnoreByRegExIgnoreSeveralErrors1] +# flags: --config-file tmp/mypy.ini +import m +x = 1 +z +l = [] +x() +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Cannot find implementation or library stub for module named 'm' + Name 'z' is not defined + Need type annotation for 'l' \(hint: "l: List\[\] = ..."\) + "int" not callable + + +[case testIgnoreByRegExIgnoreSeveralErrors2] +# flags: --config-file tmp/mypy.ini +import m +x = 1 +z +l = [] +x() +[builtins fixtures/list.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = + Cannot find implementation or library stub for module named 'm' + Name 'z' is not defined + Need type annotation for 'l' \(hint: "l: List\[\] = ..."\) + "int" not callable + + +[case testIgnoreByRegExIgnorePropertyAndDecoratorsError] +# flags: --config-file tmp/mypy.ini +def decorator(func): + return func + +class A(): + @decorator + @property + def foo(self) -> int: + return 42 + +[builtins fixtures/property.pyi] +[file mypy.ini] +\[mypy] +ignore_errors_by_regex = Decorated property not supported