Skip to content

Commit c623a85

Browse files
committed
Add --warn-unused-strictness-exceptions flag
Warn about strictness flags unnecessarily disabled for particular modules. If a flag is enabled globally (either explicitly or by default) and it is disabled for a particular module that does not have errors related to this flag mypy will output an error. Currently, this is implemented only for --disallow-any-generics but it should be very straightforward to support other strictness flags. Fixes #4018
1 parent fab9fc2 commit c623a85

File tree

7 files changed

+132
-20
lines changed

7 files changed

+132
-20
lines changed

mypy/build.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,10 @@ def __init__(self,
15301530
# TODO: Get mtime if not cached.
15311531
if self.meta is not None:
15321532
self.interface_hash = self.meta.interface_hash
1533+
for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
1534+
if (option in self.options.unused_strictness_whitelist and
1535+
not getattr(self.options, option)):
1536+
self.options.unused_strictness_whitelist[option].add(self.id)
15331537
self.add_ancestors()
15341538
self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
15351539
if self.meta:
@@ -1936,6 +1940,12 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
19361940
if manager.options.warn_unused_ignores:
19371941
# TODO: This could also be a per-module option.
19381942
manager.errors.generate_unused_ignore_notes()
1943+
if manager.options.warn_unused_strictness_exceptions:
1944+
for option in manager.options.unused_strictness_whitelist:
1945+
for file in manager.options.unused_strictness_whitelist[option]:
1946+
message = "Flag {} can be enabled for module '{}'".format(option, file)
1947+
manager.errors.report(-1, -1, message, blocker=False, severity='warning',
1948+
file=file)
19391949
manager.saved_cache.update(preserve_cache(graph))
19401950
if manager.options.dump_deps:
19411951
# This speeds up startup a little when not using the daemon mode.

mypy/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,10 @@ def add_invertible_flag(flag: str,
293293
add_invertible_flag('--warn-unused-ignores', default=False, strict_flag=True,
294294
help="warn about unneeded '# type: ignore' comments")
295295
add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True,
296-
help="warn about unnused '[mypy-<pattern>]' config sections")
296+
help="warn about unused '[mypy-<pattern>]' config sections")
297+
add_invertible_flag('--warn-unused-strictness-exceptions', default=False, strict_flag=True,
298+
help="warn about strictness flags unnecessarily disabled for particular "
299+
"modules")
297300
add_invertible_flag('--show-error-context', default=False,
298301
dest='show_error_context',
299302
help='Precede errors with "note:" messages explaining context')
@@ -497,6 +500,10 @@ def add_invertible_flag(flag: str,
497500
if options.quick_and_dirty:
498501
options.incremental = True
499502

503+
for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
504+
if getattr(options, option):
505+
options.unused_strictness_whitelist[option] = set()
506+
500507
# Set target.
501508
if special_opts.modules:
502509
options.build_type = BuildType.MODULE

mypy/options.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class Options:
4444
OPTIONS_AFFECTING_CACHE = ((PER_MODULE_OPTIONS | {"quick_and_dirty", "platform"})
4545
- {"debug_cache"})
4646

47+
WARN_UNUSED_STRICTNESS_OPTIONS = {
48+
"disallow_any_generics",
49+
}
50+
4751
def __init__(self) -> None:
4852
# -- build options --
4953
self.build_type = BuildType.STANDARD
@@ -100,6 +104,9 @@ def __init__(self) -> None:
100104
# Warn about unused '[mypy-<pattern>] config sections
101105
self.warn_unused_configs = False
102106

107+
# Warn about strictness flags unnecessarily disabled for particular modules
108+
self.warn_unused_strictness_exceptions = False
109+
103110
# Files in which to ignore all non-fatal errors
104111
self.ignore_errors = False
105112

@@ -147,6 +154,9 @@ def __init__(self) -> None:
147154
# Map pattern back to glob
148155
self.unused_configs = OrderedDict() # type: OrderedDict[Pattern[str], str]
149156

157+
# Dict of options to files in which they can be disabled
158+
self.unused_strictness_whitelist = {} # type: Dict[str, Set[str]]
159+
150160
# -- development options --
151161
self.verbosity = 0 # More verbose messages (for troubleshooting)
152162
self.pdb = False
@@ -198,3 +208,8 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:
198208

199209
def select_options_affecting_cache(self) -> Mapping[str, bool]:
200210
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}
211+
212+
def remove_from_whitelist(self, flag: str, file: Optional[str]) -> None:
213+
whitelist = self.unused_strictness_whitelist
214+
if flag in whitelist and file in whitelist[flag]:
215+
whitelist[flag].remove(file)

mypy/semanal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,7 @@ def type_analyzer(self, *,
16241624
self.note,
16251625
self.plugin,
16261626
self.options,
1627-
self.is_typeshed_stub_file,
1627+
self.errors,
16281628
aliasing=aliasing,
16291629
allow_tuple_literal=allow_tuple_literal,
16301630
allow_unnormalized=self.is_stub_file,
@@ -1733,7 +1733,7 @@ def analyze_alias(self, rvalue: Expression,
17331733
self.note,
17341734
self.plugin,
17351735
self.options,
1736-
self.is_typeshed_stub_file,
1736+
self.errors,
17371737
allow_unnormalized=True,
17381738
in_dynamic_func=dynamic,
17391739
global_scope=global_scope,

mypy/semanal_pass3.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,16 +368,20 @@ def make_type_analyzer(self, indicator: Dict[str, bool]) -> TypeAnalyserPass3:
368368
self.sem.note,
369369
self.sem.plugin,
370370
self.options,
371-
self.is_typeshed_file,
371+
self.errors,
372372
indicator)
373373

374374
def check_for_omitted_generics(self, typ: Type) -> None:
375-
if not self.options.disallow_any_generics or self.is_typeshed_file:
375+
if self.is_typeshed_file:
376376
return
377377

378378
for t in collect_any_types(typ):
379379
if t.type_of_any == TypeOfAny.from_omitted_generics:
380-
self.fail(messages.BARE_GENERIC, t)
380+
if self.options.disallow_any_generics:
381+
self.fail(messages.BARE_GENERIC, t)
382+
else:
383+
self.options.remove_from_whitelist("disallow_any_generics",
384+
self.errors.current_module())
381385

382386
def fail(self, msg: str, ctx: Context, *, blocker: bool = False) -> None:
383387
self.errors.report(ctx.get_line(), ctx.get_column(), msg)

mypy/typeanal.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import itertools
1010

11+
from mypy.errors import Errors
1112
from mypy.messages import MessageBuilder
1213
from mypy.options import Options
1314
from mypy.types import (
@@ -60,7 +61,7 @@ def analyze_type_alias(node: Expression,
6061
note_func: Callable[[str, Context], None],
6162
plugin: Plugin,
6263
options: Options,
63-
is_typeshed_stub: bool,
64+
errors: Errors,
6465
allow_unnormalized: bool = False,
6566
in_dynamic_func: bool = False,
6667
global_scope: bool = True,
@@ -117,7 +118,7 @@ def analyze_type_alias(node: Expression,
117118
fail_func('Invalid type alias', node)
118119
return None
119120
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, note_func,
120-
plugin, options, is_typeshed_stub, aliasing=True,
121+
plugin, options, errors, aliasing=True,
121122
allow_unnormalized=allow_unnormalized, warn_bound_tvar=warn_bound_tvar)
122123
analyzer.in_dynamic_func = in_dynamic_func
123124
analyzer.global_scope = global_scope
@@ -151,7 +152,7 @@ def __init__(self,
151152
note_func: Callable[[str, Context], None],
152153
plugin: Plugin,
153154
options: Options,
154-
is_typeshed_stub: bool, *,
155+
errors: Errors, *,
155156
aliasing: bool = False,
156157
allow_tuple_literal: bool = False,
157158
allow_unnormalized: bool = False,
@@ -169,7 +170,7 @@ def __init__(self,
169170
self.allow_unnormalized = allow_unnormalized
170171
self.plugin = plugin
171172
self.options = options
172-
self.is_typeshed_stub = is_typeshed_stub
173+
self.errors = errors
173174
self.warn_bound_tvar = warn_bound_tvar
174175
self.third_pass = third_pass
175176

@@ -213,8 +214,12 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
213214
elif fullname == 'typing.Tuple':
214215
if len(t.args) == 0 and not t.empty_tuple_index:
215216
# Bare 'Tuple' is same as 'tuple'
216-
if self.options.disallow_any_generics and not self.is_typeshed_stub:
217-
self.fail(messages.BARE_GENERIC, t)
217+
if not self.errors.is_typeshed_file(self.errors.file):
218+
if self.options.disallow_any_generics:
219+
self.fail(messages.BARE_GENERIC, t)
220+
else:
221+
self.options.remove_from_whitelist("disallow_any_generics",
222+
self.errors.current_module())
218223
typ = self.named_type('builtins.tuple', line=t.line, column=t.column)
219224
typ.from_generic_builtin = True
220225
return typ
@@ -653,15 +658,15 @@ def __init__(self,
653658
note_func: Callable[[str, Context], None],
654659
plugin: Plugin,
655660
options: Options,
656-
is_typeshed_stub: bool,
661+
errors: Errors,
657662
indicator: Dict[str, bool]) -> None:
658663
self.lookup_func = lookup_func
659664
self.lookup_fqn_func = lookup_fqn_func
660665
self.fail = fail_func
661666
self.note_func = note_func
662667
self.options = options
663668
self.plugin = plugin
664-
self.is_typeshed_stub = is_typeshed_stub
669+
self.errors = errors
665670
self.indicator = indicator
666671

667672
def visit_instance(self, t: Instance) -> None:
@@ -672,11 +677,13 @@ def visit_instance(self, t: Instance) -> None:
672677
if len(t.args) != len(info.type_vars):
673678
if len(t.args) == 0:
674679
from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin
675-
if (self.options.disallow_any_generics and
676-
not self.is_typeshed_stub and
677-
from_builtins):
678-
alternative = nongen_builtins[t.type.fullname()]
679-
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
680+
if from_builtins and not self.errors.is_typeshed_file(self.errors.file):
681+
if self.options.disallow_any_generics:
682+
alternative = nongen_builtins[t.type.fullname()]
683+
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
684+
else:
685+
self.options.remove_from_whitelist("disallow_any_generics",
686+
self.errors.current_module())
680687
# Insert implicit 'Any' type arguments.
681688
if from_builtins:
682689
# this 'Any' was already reported elsewhere
@@ -834,7 +841,7 @@ def anal_type(self, tp: UnboundType) -> Type:
834841
self.note_func,
835842
self.plugin,
836843
self.options,
837-
self.is_typeshed_stub,
844+
self.errors,
838845
third_pass=True)
839846
return tp.accept(tpan)
840847

test-data/unit/cmdline.test

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,3 +1008,72 @@ def get_tasks(self):
10081008
return 'whatever'
10091009
[out]
10101010
a.py:1: error: Function is missing a type annotation
1011+
1012+
-- Unused strictness exceptions
1013+
-- ----------------------------
1014+
1015+
[case testUnusedStrictnessExceptions]
1016+
# cmd: mypy a.py b.py
1017+
1018+
[file mypy.ini]
1019+
[[mypy]
1020+
warn_unused_strictness_exceptions = True
1021+
disallow_any_generics = True
1022+
1023+
[[mypy-a]
1024+
disallow_any_generics = False
1025+
1026+
[[mypy-b]
1027+
disallow_any_generics = False
1028+
1029+
[file a.py]
1030+
from typing import List
1031+
1032+
f: List = []
1033+
[file b.py]
1034+
from typing import List
1035+
1036+
f: List[int] = []
1037+
1038+
[out]
1039+
b: warning: Flag disallow_any_generics can be enabled for module 'b'
1040+
1041+
[case testUnusedStrictnessExceptionSubpattern]
1042+
# cmd: mypy test1.py test2.py
1043+
1044+
[file mypy.ini]
1045+
[[mypy]
1046+
warn_unused_strictness_exceptions = True
1047+
disallow_any_generics = True
1048+
1049+
[[mypy-test*]
1050+
disallow_any_generics = False
1051+
1052+
[file test1.py]
1053+
f: list = []
1054+
1055+
[file test2.py]
1056+
from typing import List
1057+
1058+
f: List[int] = []
1059+
1060+
[out]
1061+
test2: warning: Flag disallow_any_generics can be enabled for module 'test2'
1062+
1063+
[case testUnusedStrictnessExceptionsNotAffectedByTypeIgnore]
1064+
# cmd: mypy a.py
1065+
1066+
[file mypy.ini]
1067+
[[mypy]
1068+
warn_unused_strictness_exceptions = True
1069+
disallow_any_generics = True
1070+
1071+
[[mypy-a]
1072+
disallow_any_generics = False
1073+
1074+
[file a.py]
1075+
from typing import Tuple
1076+
1077+
f: Tuple = (1, 2, 3) # type: ignore
1078+
1079+
[out]

0 commit comments

Comments
 (0)