diff --git a/mypy/build.py b/mypy/build.py index a2d04aaf4a4e..b5a53baf844f 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -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: @@ -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 diff --git a/mypy/main.py b/mypy/main.py index 8756be3bed2b..e83ca26aa9c0 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -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-]' 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') @@ -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 diff --git a/mypy/options.py b/mypy/options.py index 5ea251df2c9d..b804f816106b 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -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] @@ -105,6 +109,9 @@ def __init__(self) -> None: # Warn about unused '[mypy-] 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 @@ -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 @@ -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) diff --git a/mypy/semanal.py b/mypy/semanal.py index ca93352c384f..b2f7075160de 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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, @@ -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, diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 808f9c576472..961a6646c013 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -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]: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8b31f86514ea..fab38e9e4099 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -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 ( @@ -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, @@ -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 @@ -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, @@ -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. @@ -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 @@ -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 @@ -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] @@ -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 @@ -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 diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index c0c706f4816b..7cda55847efe 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -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]