Skip to content

Commit 4a364ca

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 5160c9a commit 4a364ca

File tree

7 files changed

+131
-19
lines changed

7 files changed

+131
-19
lines changed

mypy/build.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,10 @@ def __init__(self,
15761576
# TODO: Get mtime if not cached.
15771577
if self.meta is not None:
15781578
self.interface_hash = self.meta.interface_hash
1579+
for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
1580+
if (option in self.options.unused_strictness_whitelist and
1581+
not getattr(self.options, option)):
1582+
self.options.unused_strictness_whitelist[option].add(self.id)
15791583
self.add_ancestors()
15801584
self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
15811585
if self.meta:
@@ -2055,6 +2059,12 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
20552059
else:
20562060
process_graph(graph, manager)
20572061

2062+
if manager.options.warn_unused_strictness_exceptions:
2063+
for option in manager.options.unused_strictness_whitelist:
2064+
for file in manager.options.unused_strictness_whitelist[option]:
2065+
message = "Flag {} can be enabled for module '{}'".format(option, file)
2066+
manager.errors.report(-1, -1, message, blocker=False, severity='warning',
2067+
file=file)
20582068
if manager.options.dump_deps:
20592069
# This speeds up startup a little when not using the daemon mode.
20602070
from mypy.server.deps import dump_all_dependencies

mypy/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ def add_invertible_flag(flag: str,
313313
help="warn about unneeded '# type: ignore' comments")
314314
add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True,
315315
help="warn about unused '[mypy-<pattern>]' config sections")
316+
add_invertible_flag('--warn-unused-strictness-exceptions', default=False, strict_flag=True,
317+
help="warn about strictness flags unnecessarily disabled for particular "
318+
"modules")
316319
add_invertible_flag('--show-error-context', default=False,
317320
dest='show_error_context',
318321
help='Precede errors with "note:" messages explaining context')
@@ -524,6 +527,10 @@ def add_invertible_flag(flag: str,
524527
if options.quick_and_dirty:
525528
options.incremental = True
526529

530+
for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
531+
if getattr(options, option):
532+
options.unused_strictness_whitelist[option] = set()
533+
527534
# Set target.
528535
if special_opts.modules + special_opts.packages:
529536
options.build_type = BuildType.MODULE

mypy/options.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ class Options:
4646
{"quick_and_dirty", "platform", "cache_fine_grained"})
4747
- {"debug_cache"})
4848

49+
WARN_UNUSED_STRICTNESS_OPTIONS = {
50+
"disallow_any_generics",
51+
}
52+
4953
def __init__(self) -> None:
5054
# Cache for clone_for_module()
5155
self.clone_cache = {} # type: Dict[str, Options]
@@ -105,6 +109,9 @@ def __init__(self) -> None:
105109
# Warn about unused '[mypy-<pattern>] config sections
106110
self.warn_unused_configs = False
107111

112+
# Warn about strictness flags unnecessarily disabled for particular modules
113+
self.warn_unused_strictness_exceptions = False
114+
108115
# Files in which to ignore all non-fatal errors
109116
self.ignore_errors = False
110117

@@ -155,6 +162,9 @@ def __init__(self) -> None:
155162
# Map pattern back to glob
156163
self.unused_configs = OrderedDict() # type: OrderedDict[Pattern[str], str]
157164

165+
# Dict of options to files in which they can be disabled
166+
self.unused_strictness_whitelist = {} # type: Dict[str, Set[str]]
167+
158168
# -- development options --
159169
self.verbosity = 0 # More verbose messages (for troubleshooting)
160170
self.pdb = False
@@ -220,3 +230,8 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:
220230

221231
def select_options_affecting_cache(self) -> Mapping[str, bool]:
222232
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}
233+
234+
def remove_from_whitelist(self, flag: str, file: Optional[str]) -> None:
235+
whitelist = self.unused_strictness_whitelist
236+
if flag in whitelist and file in whitelist[flag]:
237+
whitelist[flag].remove(file)

mypy/semanal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,7 +1732,7 @@ def type_analyzer(self, *,
17321732
tvar_scope,
17331733
self.plugin,
17341734
self.options,
1735-
self.is_typeshed_stub_file,
1735+
self.errors,
17361736
aliasing=aliasing,
17371737
allow_tuple_literal=allow_tuple_literal,
17381738
allow_unnormalized=self.is_stub_file,
@@ -1862,7 +1862,7 @@ def analyze_alias(self, rvalue: Expression,
18621862
self.tvar_scope,
18631863
self.plugin,
18641864
self.options,
1865-
self.is_typeshed_stub_file,
1865+
self.errors,
18661866
allow_unnormalized=True,
18671867
in_dynamic_func=dynamic,
18681868
global_scope=global_scope,

mypy/semanal_pass3.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,17 +430,21 @@ def make_type_analyzer(self, indicator: Dict[str, bool]) -> TypeAnalyserPass3:
430430
return TypeAnalyserPass3(self,
431431
self.sem.plugin,
432432
self.options,
433-
self.is_typeshed_file,
433+
self.errors,
434434
indicator,
435435
self.patches)
436436

437437
def check_for_omitted_generics(self, typ: Type) -> None:
438-
if not self.options.disallow_any_generics or self.is_typeshed_file:
438+
if self.is_typeshed_file:
439439
return
440440

441441
for t in collect_any_types(typ):
442442
if t.type_of_any == TypeOfAny.from_omitted_generics:
443-
self.fail(messages.BARE_GENERIC, t)
443+
if self.options.disallow_any_generics:
444+
self.fail(messages.BARE_GENERIC, t)
445+
else:
446+
self.options.remove_from_whitelist("disallow_any_generics",
447+
self.errors.current_module())
444448

445449
def lookup_qualified(self, name: str, ctx: Context,
446450
suppress_errors: bool = False) -> Optional[SymbolTableNode]:

mypy/typeanal.py

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

1111
import itertools
1212

13+
from mypy.errors import Errors
1314
from mypy.messages import MessageBuilder
1415
from mypy.options import Options
1516
from mypy.types import (
@@ -59,7 +60,7 @@ def analyze_type_alias(node: Expression,
5960
tvar_scope: TypeVarScope,
6061
plugin: Plugin,
6162
options: Options,
62-
is_typeshed_stub: bool,
63+
errors: Errors,
6364
allow_unnormalized: bool = False,
6465
in_dynamic_func: bool = False,
6566
global_scope: bool = True,
@@ -117,7 +118,7 @@ def analyze_type_alias(node: Expression,
117118
except TypeTranslationError:
118119
api.fail('Invalid type alias', node)
119120
return None
120-
analyzer = TypeAnalyser(api, tvar_scope, plugin, options, is_typeshed_stub, aliasing=True,
121+
analyzer = TypeAnalyser(api, tvar_scope, 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
@@ -149,7 +150,7 @@ def __init__(self,
149150
tvar_scope: Optional[TypeVarScope],
150151
plugin: Plugin,
151152
options: Options,
152-
is_typeshed_stub: bool, *,
153+
errors: Errors, *,
153154
aliasing: bool = False,
154155
allow_tuple_literal: bool = False,
155156
allow_unnormalized: bool = False,
@@ -168,7 +169,7 @@ def __init__(self,
168169
self.allow_unnormalized = allow_unnormalized
169170
self.plugin = plugin
170171
self.options = options
171-
self.is_typeshed_stub = is_typeshed_stub
172+
self.errors = errors
172173
self.warn_bound_tvar = warn_bound_tvar
173174
self.third_pass = third_pass
174175
# Names of type aliases encountered while analysing a type will be collected here.
@@ -235,8 +236,12 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
235236
elif fullname == 'typing.Tuple':
236237
if len(t.args) == 0 and not t.empty_tuple_index:
237238
# Bare 'Tuple' is same as 'tuple'
238-
if self.options.disallow_any_generics and not self.is_typeshed_stub:
239-
self.fail(messages.BARE_GENERIC, t)
239+
if not self.errors.is_typeshed_file(self.errors.file):
240+
if self.options.disallow_any_generics:
241+
self.fail(messages.BARE_GENERIC, t)
242+
else:
243+
self.options.remove_from_whitelist("disallow_any_generics",
244+
self.errors.current_module())
240245
typ = self.named_type('builtins.tuple', line=t.line, column=t.column)
241246
typ.from_generic_builtin = True
242247
return typ
@@ -680,7 +685,7 @@ def __init__(self,
680685
api: SemanticAnalyzerInterface,
681686
plugin: Plugin,
682687
options: Options,
683-
is_typeshed_stub: bool,
688+
errors: Errors,
684689
indicator: Dict[str, bool],
685690
patches: List[Tuple[int, Callable[[], None]]]) -> None:
686691
self.api = api
@@ -690,7 +695,7 @@ def __init__(self,
690695
self.note_func = api.note
691696
self.options = options
692697
self.plugin = plugin
693-
self.is_typeshed_stub = is_typeshed_stub
698+
self.errors = errors
694699
self.indicator = indicator
695700
self.patches = patches
696701
self.aliases_used = set() # type: Set[str]
@@ -703,11 +708,13 @@ def visit_instance(self, t: Instance) -> None:
703708
if len(t.args) != len(info.type_vars):
704709
if len(t.args) == 0:
705710
from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin
706-
if (self.options.disallow_any_generics and
707-
not self.is_typeshed_stub and
708-
from_builtins):
709-
alternative = nongen_builtins[t.type.fullname()]
710-
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
711+
if from_builtins and not self.errors.is_typeshed_file(self.errors.file):
712+
if self.options.disallow_any_generics:
713+
alternative = nongen_builtins[t.type.fullname()]
714+
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
715+
else:
716+
self.options.remove_from_whitelist("disallow_any_generics",
717+
self.errors.current_module())
711718
# Insert implicit 'Any' type arguments.
712719
if from_builtins:
713720
# this 'Any' was already reported elsewhere
@@ -819,7 +826,7 @@ def anal_type(self, tp: UnboundType) -> Type:
819826
None,
820827
self.plugin,
821828
self.options,
822-
self.is_typeshed_stub,
829+
self.errors,
823830
third_pass=True)
824831
res = tp.accept(tpan)
825832
self.aliases_used = tpan.aliases_used

test-data/unit/cmdline.test

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,3 +1088,72 @@ p.b.bar("wrong")
10881088
p/a.py:4: error: Argument 1 to "foo" has incompatible type "str"; expected "int"
10891089
p/b/__init__.py:5: error: Argument 1 to "bar" has incompatible type "str"; expected "int"
10901090
c.py:2: error: Argument 1 to "bar" has incompatible type "str"; expected "int"
1091+
1092+
-- Unused strictness exceptions
1093+
-- ----------------------------
1094+
1095+
[case testUnusedStrictnessExceptions]
1096+
# cmd: mypy a.py b.py
1097+
1098+
[file mypy.ini]
1099+
[[mypy]
1100+
warn_unused_strictness_exceptions = True
1101+
disallow_any_generics = True
1102+
1103+
[[mypy-a]
1104+
disallow_any_generics = False
1105+
1106+
[[mypy-b]
1107+
disallow_any_generics = False
1108+
1109+
[file a.py]
1110+
from typing import List
1111+
1112+
f: List = []
1113+
[file b.py]
1114+
from typing import List
1115+
1116+
f: List[int] = []
1117+
1118+
[out]
1119+
b: warning: Flag disallow_any_generics can be enabled for module 'b'
1120+
1121+
[case testUnusedStrictnessExceptionSubpattern]
1122+
# cmd: mypy test1.py test2.py
1123+
1124+
[file mypy.ini]
1125+
[[mypy]
1126+
warn_unused_strictness_exceptions = True
1127+
disallow_any_generics = True
1128+
1129+
[[mypy-test*]
1130+
disallow_any_generics = False
1131+
1132+
[file test1.py]
1133+
f: list = []
1134+
1135+
[file test2.py]
1136+
from typing import List
1137+
1138+
f: List[int] = []
1139+
1140+
[out]
1141+
test2: warning: Flag disallow_any_generics can be enabled for module 'test2'
1142+
1143+
[case testUnusedStrictnessExceptionsNotAffectedByTypeIgnore]
1144+
# cmd: mypy a.py
1145+
1146+
[file mypy.ini]
1147+
[[mypy]
1148+
warn_unused_strictness_exceptions = True
1149+
disallow_any_generics = True
1150+
1151+
[[mypy-a]
1152+
disallow_any_generics = False
1153+
1154+
[file a.py]
1155+
from typing import Tuple
1156+
1157+
f: Tuple = (1, 2, 3) # type: ignore
1158+
1159+
[out]

0 commit comments

Comments
 (0)