Skip to content

Commit e056774

Browse files
committed
Restrict the number of errors shown when there are missing stubs (#10579)
When upgrading to mypy 0.900, most projects will see some errors about missing stubs. Projects with strict settings could see thousands of errors, since missing stubs will generate many additional Any types. After 200 errors (only if some of them are about imports) we will now only show errors about unresolved imports or missing stubs, so that the likely root causes won't be obscured in a high volume of errors. Fixes #10529.
1 parent 56d5b3a commit e056774

File tree

7 files changed

+165
-4
lines changed

7 files changed

+165
-4
lines changed

docs/source/command_line.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,14 @@ in error messages.
687687

688688
Show absolute paths to files.
689689

690+
.. option:: --soft-error-limit N
691+
692+
This flag will adjust the limit after which mypy will (sometimes)
693+
disable reporting most additional errors. The limit only applies
694+
if it seems likely that most of the remaining errors will not be
695+
useful or they may be overly noisy. If ``N`` is negative, there is
696+
no limit. The default limit is 200.
697+
690698

691699
.. _incremental:
692700

mypy/build.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ def _build(sources: List[BuildSource],
224224
lambda path: read_py_file(path, cached_read, options.python_version),
225225
options.show_absolute_path,
226226
options.enabled_error_codes,
227-
options.disabled_error_codes)
227+
options.disabled_error_codes,
228+
options.many_errors_threshold)
228229
plugin, snapshot = load_plugins(options, errors, stdout, extra_plugins)
229230

230231
# Add catch-all .gitignore to cache dir if we created it

mypy/defaults.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@
3030
'html',
3131
'txt',
3232
'lineprecision'] # type: Final
33+
34+
# Threshold after which we sometimes filter out most errors to avoid very
35+
# verbose output
36+
MANY_ERRORS_THRESHOLD = 200 # type: Final

mypy/errors.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from mypy.scope import Scope
1111
from mypy.options import Options
1212
from mypy.version import __version__ as mypy_version
13-
from mypy.errorcodes import ErrorCode
13+
from mypy.errorcodes import ErrorCode, IMPORT
1414
from mypy import errorcodes as codes
1515
from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file
1616

@@ -65,6 +65,10 @@ class ErrorInfo:
6565
# Fine-grained incremental target where this was reported
6666
target = None # type: Optional[str]
6767

68+
# If True, don't show this message in output, but still record the error (needed
69+
# by mypy daemon)
70+
hidden = False
71+
6872
def __init__(self,
6973
import_ctx: List[Tuple[str, int]],
7074
file: str,
@@ -158,6 +162,10 @@ class Errors:
158162
target_module = None # type: Optional[str]
159163
scope = None # type: Optional[Scope]
160164

165+
# Have we seen an import-related error so far? If yes, we filter out other messages
166+
# in some cases to avoid reporting huge numbers of errors.
167+
seen_import_error = False
168+
161169
def __init__(self,
162170
show_error_context: bool = False,
163171
show_column_numbers: bool = False,
@@ -166,7 +174,8 @@ def __init__(self,
166174
read_source: Optional[Callable[[str], Optional[List[str]]]] = None,
167175
show_absolute_path: bool = False,
168176
enabled_error_codes: Optional[Set[ErrorCode]] = None,
169-
disabled_error_codes: Optional[Set[ErrorCode]] = None) -> None:
177+
disabled_error_codes: Optional[Set[ErrorCode]] = None,
178+
many_errors_threshold: int = -1) -> None:
170179
self.show_error_context = show_error_context
171180
self.show_column_numbers = show_column_numbers
172181
self.show_error_codes = show_error_codes
@@ -176,6 +185,7 @@ def __init__(self,
176185
self.read_source = read_source
177186
self.enabled_error_codes = enabled_error_codes or set()
178187
self.disabled_error_codes = disabled_error_codes or set()
188+
self.many_errors_threshold = many_errors_threshold
179189
self.initialize()
180190

181191
def initialize(self) -> None:
@@ -189,6 +199,7 @@ def initialize(self) -> None:
189199
self.only_once_messages = set()
190200
self.scope = None
191201
self.target_module = None
202+
self.seen_import_error = False
192203

193204
def reset(self) -> None:
194205
self.initialize()
@@ -201,12 +212,14 @@ def copy(self) -> 'Errors':
201212
self.read_source,
202213
self.show_absolute_path,
203214
self.enabled_error_codes,
204-
self.disabled_error_codes)
215+
self.disabled_error_codes,
216+
self.many_errors_threshold)
205217
new.file = self.file
206218
new.import_ctx = self.import_ctx[:]
207219
new.function_or_member = self.function_or_member[:]
208220
new.target_module = self.target_module
209221
new.scope = self.scope
222+
new.seen_import_error = self.seen_import_error
210223
return new
211224

212225
def total_errors(self) -> int:
@@ -330,6 +343,8 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None:
330343
if file not in self.error_info_map:
331344
self.error_info_map[file] = []
332345
self.error_info_map[file].append(info)
346+
if info.code is IMPORT:
347+
self.seen_import_error = True
333348

334349
def add_error_info(self, info: ErrorInfo) -> None:
335350
file, line, end_line = info.origin
@@ -354,8 +369,52 @@ def add_error_info(self, info: ErrorInfo) -> None:
354369
if info.message in self.only_once_messages:
355370
return
356371
self.only_once_messages.add(info.message)
372+
if self.seen_import_error and info.code is not IMPORT and self.has_many_errors():
373+
# Missing stubs can easily cause thousands of errors about
374+
# Any types, especially when upgrading to mypy 0.900,
375+
# which no longer bundles third-party library stubs. Avoid
376+
# showing too many errors to make it easier to see
377+
# import-related errors.
378+
info.hidden = True
379+
self.report_hidden_errors(info)
357380
self._add_error_info(file, info)
358381

382+
def has_many_errors(self) -> bool:
383+
if self.many_errors_threshold < 0:
384+
return False
385+
if len(self.error_info_map) >= self.many_errors_threshold:
386+
return True
387+
if sum(len(errors)
388+
for errors in self.error_info_map.values()) >= self.many_errors_threshold:
389+
return True
390+
return False
391+
392+
def report_hidden_errors(self, info: ErrorInfo) -> None:
393+
message = (
394+
'(Skipping most remaining errors due to unresolved imports or missing stubs; ' +
395+
'fix these first)'
396+
)
397+
if message in self.only_once_messages:
398+
return
399+
self.only_once_messages.add(message)
400+
new_info = ErrorInfo(
401+
import_ctx=info.import_ctx,
402+
file=info.file,
403+
module=info.module,
404+
typ=None,
405+
function_or_member=None,
406+
line=info.line,
407+
column=info.line,
408+
severity='note',
409+
message=message,
410+
code=None,
411+
blocker=False,
412+
only_once=True,
413+
origin=info.origin,
414+
target=info.target,
415+
)
416+
self._add_error_info(info.origin[0], new_info)
417+
359418
def is_ignored_error(self, line: int, info: ErrorInfo, ignores: Dict[int, List[str]]) -> bool:
360419
if info.blocker:
361420
# Blocking errors can never be ignored
@@ -453,6 +512,7 @@ def format_messages(self, error_info: List[ErrorInfo],
453512
severity 'error').
454513
"""
455514
a = [] # type: List[str]
515+
error_info = [info for info in error_info if not info.hidden]
456516
errors = self.render_messages(self.sort_messages(error_info))
457517
errors = self.remove_duplicates(errors)
458518
for file, line, column, severity, message, code in errors:

mypy/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,8 @@ def add_invertible_flag(flag: str,
663663
add_invertible_flag('--show-absolute-path', default=False,
664664
help="Show absolute paths to files",
665665
group=error_group)
666+
error_group.add_argument('--soft-error-limit', default=defaults.MANY_ERRORS_THRESHOLD,
667+
type=int, dest="many_errors_threshold", help=argparse.SUPPRESS)
666668

667669
incremental_group = parser.add_argument_group(
668670
title='Incremental mode',

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ def __init__(self) -> None:
295295
self.show_absolute_path = False # type: bool
296296
# Install missing stub packages if True
297297
self.install_types = False
298+
# When we encounter errors that may cause many additional errors,
299+
# skip most errors after this many messages have been reported.
300+
# -1 means unlimited.
301+
self.many_errors_threshold = defaults.MANY_ERRORS_THRESHOLD
298302

299303
# To avoid breaking plugin compatibility, keep providing new_semantic_analyzer
300304
@property

test-data/unit/check-modules.test

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2982,3 +2982,85 @@ T = TypeVar("T")
29822982
class F(M):
29832983
x: C
29842984
class C: ...
2985+
2986+
[case testLimitLegacyStubErrorVolume]
2987+
# flags: --disallow-any-expr --soft-error-limit=5
2988+
import certifi # E: Cannot find implementation or library stub for module named "certifi" \
2989+
# N: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
2990+
certifi.x # E: Expression has type "Any"
2991+
certifi.x # E: Expression has type "Any"
2992+
certifi.x # E: Expression has type "Any"
2993+
certifi.x # N: (Skipping most remaining errors due to unresolved imports or missing stubs; fix these first)
2994+
certifi.x
2995+
certifi.x
2996+
certifi.x
2997+
certifi.x
2998+
2999+
[case testDoNotLimitErrorVolumeIfNotImportErrors]
3000+
# flags: --disallow-any-expr --soft-error-limit=5
3001+
def f(): pass
3002+
certifi = f() # E: Expression has type "Any"
3003+
1() # E: "int" not callable
3004+
certifi.x # E: Expression has type "Any"
3005+
certifi.x # E: Expression has type "Any"
3006+
certifi.x # E: Expression has type "Any"
3007+
certifi.x # E: Expression has type "Any"
3008+
certifi.x # E: Expression has type "Any"
3009+
certifi.x # E: Expression has type "Any"
3010+
certifi.x # E: Expression has type "Any"
3011+
certifi.x # E: Expression has type "Any"
3012+
1() # E: "int" not callable
3013+
3014+
3015+
[case testDoNotLimitImportErrorVolume]
3016+
# flags: --disallow-any-expr --soft-error-limit=3
3017+
import xyz1 # E: Cannot find implementation or library stub for module named "xyz1" \
3018+
# N: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
3019+
import xyz2 # E: Cannot find implementation or library stub for module named "xyz2"
3020+
import xyz3 # E: Cannot find implementation or library stub for module named "xyz3"
3021+
import xyz4 # E: Cannot find implementation or library stub for module named "xyz4"
3022+
3023+
[case testUnlimitedStubErrorVolume]
3024+
# flags: --disallow-any-expr --soft-error-limit=-1
3025+
import certifi # E: Cannot find implementation or library stub for module named "certifi" \
3026+
# N: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
3027+
certifi.x # E: Expression has type "Any"
3028+
certifi.x # E: Expression has type "Any"
3029+
certifi.x # E: Expression has type "Any"
3030+
certifi.x # E: Expression has type "Any"
3031+
certifi.x # E: Expression has type "Any"
3032+
certifi.x # E: Expression has type "Any"
3033+
certifi.x # E: Expression has type "Any"
3034+
certifi.x # E: Expression has type "Any"
3035+
certifi.x # E: Expression has type "Any"
3036+
certifi.x # E: Expression has type "Any"
3037+
certifi.x # E: Expression has type "Any"
3038+
certifi.x # E: Expression has type "Any"
3039+
certifi.x # E: Expression has type "Any"
3040+
certifi.x # E: Expression has type "Any"
3041+
certifi.x # E: Expression has type "Any"
3042+
certifi.x # E: Expression has type "Any"
3043+
certifi.x # E: Expression has type "Any"
3044+
certifi.x # E: Expression has type "Any"
3045+
certifi.x # E: Expression has type "Any"
3046+
certifi.x # E: Expression has type "Any"
3047+
certifi.x # E: Expression has type "Any"
3048+
certifi.x # E: Expression has type "Any"
3049+
certifi.x # E: Expression has type "Any"
3050+
certifi.x # E: Expression has type "Any"
3051+
certifi.x # E: Expression has type "Any"
3052+
certifi.x # E: Expression has type "Any"
3053+
certifi.x # E: Expression has type "Any"
3054+
certifi.x # E: Expression has type "Any"
3055+
certifi.x # E: Expression has type "Any"
3056+
certifi.x # E: Expression has type "Any"
3057+
certifi.x # E: Expression has type "Any"
3058+
certifi.x # E: Expression has type "Any"
3059+
certifi.x # E: Expression has type "Any"
3060+
certifi.x # E: Expression has type "Any"
3061+
certifi.x # E: Expression has type "Any"
3062+
certifi.x # E: Expression has type "Any"
3063+
certifi.x # E: Expression has type "Any"
3064+
certifi.x # E: Expression has type "Any"
3065+
certifi.x # E: Expression has type "Any"
3066+
certifi.x # E: Expression has type "Any"

0 commit comments

Comments
 (0)