Skip to content

Add --warn-unused-strictness-exceptions flag #4225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,10 @@ def __init__(self,
# TODO: Get mtime if not cached.
if self.meta is not None:
self.interface_hash = self.meta.interface_hash
for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
if (option in self.options.unused_strictness_whitelist and
not getattr(self.options, option)):
self.options.unused_strictness_whitelist[option].add(self.id)
self.add_ancestors()
self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
if self.meta:
Expand Down Expand Up @@ -2055,6 +2059,12 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
else:
process_graph(graph, manager)

if manager.options.warn_unused_strictness_exceptions:
for option in manager.options.unused_strictness_whitelist:
for file in manager.options.unused_strictness_whitelist[option]:
message = "Flag {} can be enabled for module '{}'".format(option, file)
manager.errors.report(-1, -1, message, blocker=False, severity='warning',
file=file)
if manager.options.dump_deps:
# This speeds up startup a little when not using the daemon mode.
from mypy.server.deps import dump_all_dependencies
Expand Down
7 changes: 7 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ def add_invertible_flag(flag: str,
help="warn about unneeded '# type: ignore' comments")
add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True,
help="warn about unused '[mypy-<pattern>]' config sections")
add_invertible_flag('--warn-unused-strictness-exceptions', default=False, strict_flag=True,
help="warn about strictness flags unnecessarily disabled for particular "
"modules")
add_invertible_flag('--show-error-context', default=False,
dest='show_error_context',
help='Precede errors with "note:" messages explaining context')
Expand Down Expand Up @@ -524,6 +527,10 @@ def add_invertible_flag(flag: str,
if options.quick_and_dirty:
options.incremental = True

for option in Options.WARN_UNUSED_STRICTNESS_OPTIONS:
if getattr(options, option):
options.unused_strictness_whitelist[option] = set()

# Set target.
if special_opts.modules + special_opts.packages:
options.build_type = BuildType.MODULE
Expand Down
15 changes: 15 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class Options:
{"quick_and_dirty", "platform", "cache_fine_grained"})
- {"debug_cache"})

WARN_UNUSED_STRICTNESS_OPTIONS = {
"disallow_any_generics",
}

def __init__(self) -> None:
# Cache for clone_for_module()
self.clone_cache = {} # type: Dict[str, Options]
Expand Down Expand Up @@ -105,6 +109,9 @@ def __init__(self) -> None:
# Warn about unused '[mypy-<pattern>] config sections
self.warn_unused_configs = False

# Warn about strictness flags unnecessarily disabled for particular modules
self.warn_unused_strictness_exceptions = False

# Files in which to ignore all non-fatal errors
self.ignore_errors = False

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

# Dict of options to files in which they can be disabled
self.unused_strictness_whitelist = {} # type: Dict[str, Set[str]]

# -- development options --
self.verbosity = 0 # More verbose messages (for troubleshooting)
self.pdb = False
Expand Down Expand Up @@ -220,3 +230,8 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:

def select_options_affecting_cache(self) -> Mapping[str, bool]:
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}

def remove_from_whitelist(self, flag: str, file: Optional[str]) -> None:
whitelist = self.unused_strictness_whitelist
if flag in whitelist and file in whitelist[flag]:
whitelist[flag].remove(file)
4 changes: 2 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1732,7 +1732,7 @@ def type_analyzer(self, *,
tvar_scope,
self.plugin,
self.options,
self.is_typeshed_stub_file,
self.errors,
aliasing=aliasing,
allow_tuple_literal=allow_tuple_literal,
allow_unnormalized=self.is_stub_file,
Expand Down Expand Up @@ -1862,7 +1862,7 @@ def analyze_alias(self, rvalue: Expression,
self.tvar_scope,
self.plugin,
self.options,
self.is_typeshed_stub_file,
self.errors,
allow_unnormalized=True,
in_dynamic_func=dynamic,
global_scope=global_scope,
Expand Down
10 changes: 7 additions & 3 deletions mypy/semanal_pass3.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,17 +430,21 @@ def make_type_analyzer(self, indicator: Dict[str, bool]) -> TypeAnalyserPass3:
return TypeAnalyserPass3(self,
self.sem.plugin,
self.options,
self.is_typeshed_file,
self.errors,
indicator,
self.patches)

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

for t in collect_any_types(typ):
if t.type_of_any == TypeOfAny.from_omitted_generics:
self.fail(messages.BARE_GENERIC, t)
if self.options.disallow_any_generics:
self.fail(messages.BARE_GENERIC, t)
else:
self.options.remove_from_whitelist("disallow_any_generics",
self.errors.current_module())

def lookup_qualified(self, name: str, ctx: Context,
suppress_errors: bool = False) -> Optional[SymbolTableNode]:
Expand Down
35 changes: 21 additions & 14 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import itertools

from mypy.errors import Errors
from mypy.messages import MessageBuilder
from mypy.options import Options
from mypy.types import (
Expand Down Expand Up @@ -59,7 +60,7 @@ def analyze_type_alias(node: Expression,
tvar_scope: TypeVarScope,
plugin: Plugin,
options: Options,
is_typeshed_stub: bool,
errors: Errors,
allow_unnormalized: bool = False,
in_dynamic_func: bool = False,
global_scope: bool = True,
Expand Down Expand Up @@ -117,7 +118,7 @@ def analyze_type_alias(node: Expression,
except TypeTranslationError:
api.fail('Invalid type alias', node)
return None
analyzer = TypeAnalyser(api, tvar_scope, plugin, options, is_typeshed_stub, aliasing=True,
analyzer = TypeAnalyser(api, tvar_scope, plugin, options, errors, aliasing=True,
allow_unnormalized=allow_unnormalized, warn_bound_tvar=warn_bound_tvar)
analyzer.in_dynamic_func = in_dynamic_func
analyzer.global_scope = global_scope
Expand Down Expand Up @@ -149,7 +150,7 @@ def __init__(self,
tvar_scope: Optional[TypeVarScope],
plugin: Plugin,
options: Options,
is_typeshed_stub: bool, *,
errors: Errors, *,
aliasing: bool = False,
allow_tuple_literal: bool = False,
allow_unnormalized: bool = False,
Expand All @@ -168,7 +169,7 @@ def __init__(self,
self.allow_unnormalized = allow_unnormalized
self.plugin = plugin
self.options = options
self.is_typeshed_stub = is_typeshed_stub
self.errors = errors
self.warn_bound_tvar = warn_bound_tvar
self.third_pass = third_pass
# Names of type aliases encountered while analysing a type will be collected here.
Expand Down Expand Up @@ -235,8 +236,12 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
elif fullname == 'typing.Tuple':
if len(t.args) == 0 and not t.empty_tuple_index:
# Bare 'Tuple' is same as 'tuple'
if self.options.disallow_any_generics and not self.is_typeshed_stub:
self.fail(messages.BARE_GENERIC, t)
if not self.errors.is_typeshed_file(self.errors.file):
if self.options.disallow_any_generics:
self.fail(messages.BARE_GENERIC, t)
else:
self.options.remove_from_whitelist("disallow_any_generics",
self.errors.current_module())
typ = self.named_type('builtins.tuple', line=t.line, column=t.column)
typ.from_generic_builtin = True
return typ
Expand Down Expand Up @@ -680,7 +685,7 @@ def __init__(self,
api: SemanticAnalyzerInterface,
plugin: Plugin,
options: Options,
is_typeshed_stub: bool,
errors: Errors,
indicator: Dict[str, bool],
patches: List[Tuple[int, Callable[[], None]]]) -> None:
self.api = api
Expand All @@ -690,7 +695,7 @@ def __init__(self,
self.note_func = api.note
self.options = options
self.plugin = plugin
self.is_typeshed_stub = is_typeshed_stub
self.errors = errors
self.indicator = indicator
self.patches = patches
self.aliases_used = set() # type: Set[str]
Expand All @@ -703,11 +708,13 @@ def visit_instance(self, t: Instance) -> None:
if len(t.args) != len(info.type_vars):
if len(t.args) == 0:
from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin
if (self.options.disallow_any_generics and
not self.is_typeshed_stub and
from_builtins):
alternative = nongen_builtins[t.type.fullname()]
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
if from_builtins and not self.errors.is_typeshed_file(self.errors.file):
if self.options.disallow_any_generics:
alternative = nongen_builtins[t.type.fullname()]
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
else:
self.options.remove_from_whitelist("disallow_any_generics",
self.errors.current_module())
# Insert implicit 'Any' type arguments.
if from_builtins:
# this 'Any' was already reported elsewhere
Expand Down Expand Up @@ -819,7 +826,7 @@ def anal_type(self, tp: UnboundType) -> Type:
None,
self.plugin,
self.options,
self.is_typeshed_stub,
self.errors,
third_pass=True)
res = tp.accept(tpan)
self.aliases_used = tpan.aliases_used
Expand Down
69 changes: 69 additions & 0 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -1088,3 +1088,72 @@ p.b.bar("wrong")
p/a.py:4: error: Argument 1 to "foo" has incompatible type "str"; expected "int"
p/b/__init__.py:5: error: Argument 1 to "bar" has incompatible type "str"; expected "int"
c.py:2: error: Argument 1 to "bar" has incompatible type "str"; expected "int"

-- Unused strictness exceptions
-- ----------------------------

[case testUnusedStrictnessExceptions]
# cmd: mypy a.py b.py

[file mypy.ini]
[[mypy]
warn_unused_strictness_exceptions = True
disallow_any_generics = True

[[mypy-a]
disallow_any_generics = False

[[mypy-b]
disallow_any_generics = False

[file a.py]
from typing import List

f: List = []
[file b.py]
from typing import List

f: List[int] = []

[out]
b: warning: Flag disallow_any_generics can be enabled for module 'b'

[case testUnusedStrictnessExceptionSubpattern]
# cmd: mypy test1.py test2.py

[file mypy.ini]
[[mypy]
warn_unused_strictness_exceptions = True
disallow_any_generics = True

[[mypy-test*]
disallow_any_generics = False

[file test1.py]
f: list = []

[file test2.py]
from typing import List

f: List[int] = []

[out]
test2: warning: Flag disallow_any_generics can be enabled for module 'test2'

[case testUnusedStrictnessExceptionsNotAffectedByTypeIgnore]
# cmd: mypy a.py

[file mypy.ini]
[[mypy]
warn_unused_strictness_exceptions = True
disallow_any_generics = True

[[mypy-a]
disallow_any_generics = False

[file a.py]
from typing import Tuple

f: Tuple = (1, 2, 3) # type: ignore

[out]