Skip to content

Commit 29848f8

Browse files
committed
Add flag to disallow implicit Any types.
These types can appear from an unanalyzed module. If mypy encounters a type annotation that uses such a type, it will report an error. Fixes #3205
1 parent 266a63f commit 29848f8

File tree

8 files changed

+117
-7
lines changed

8 files changed

+117
-7
lines changed

mypy/build.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,9 +1387,7 @@ def fix_suppressed_dependencies(self, graph: Graph) -> None:
13871387
"""
13881388
# TODO: See if it's possible to move this check directly into parse_file in some way.
13891389
# TODO: Find a way to write a test case for this fix.
1390-
silent_mode = (self.options.ignore_missing_imports or
1391-
self.options.follow_imports == 'skip')
1392-
if not silent_mode:
1390+
if not self.options.silent_mode():
13931391
return
13941392

13951393
new_suppressed = []

mypy/checker.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ARG_POS, MDEF,
3030
CONTRAVARIANT, COVARIANT)
3131
from mypy import nodes
32+
from mypy.typeanal import has_any_from_silent_import
3233
from mypy.types import (
3334
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
3435
Instance, NoneTyp, strip_type, TypeType,
@@ -611,6 +612,18 @@ def is_implicit_any(t: Type) -> bool:
611612
self.fail(messages.RETURN_TYPE_EXPECTED, fdef)
612613
if any(is_implicit_any(t) for t in fdef.type.arg_types):
613614
self.fail(messages.ARGUMENT_TYPE_EXPECTED, fdef)
615+
if self.options.disallow_implicit_any_types:
616+
if fdef.type and isinstance(fdef.type, CallableType):
617+
if has_any_from_silent_import(fdef.type.ret_type):
618+
self.fail("Return type is implicitly converted to "
619+
"'{}' due to import from "
620+
"unanalyzed module".format(fdef.type.ret_type), fdef)
621+
for idx, arg_type in enumerate(fdef.type.arg_types):
622+
if has_any_from_silent_import(arg_type):
623+
self.fail("Argument {} to '{}' is implicitly converted to "
624+
"'{}' due to import from unanalyzed "
625+
"module".format(idx + 1, fdef.name(), arg_type),
626+
fdef)
614627

615628
if name in nodes.reverse_op_method_set:
616629
self.check_reverse_op_method(item, typ, name)
@@ -1706,6 +1719,10 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Expression,
17061719
self.msg.deleted_as_rvalue(rvalue_type, context)
17071720
if isinstance(lvalue_type, DeletedType):
17081721
self.msg.deleted_as_lvalue(lvalue_type, context)
1722+
elif (self.options.disallow_implicit_any_types
1723+
and has_any_from_silent_import(lvalue_type)):
1724+
self.msg.fail("Type of {} is implicitly converted to '{}' due to import from "
1725+
"unanalyzed module".format(lvalue_name, lvalue_type), context)
17091726
else:
17101727
self.check_subtype(rvalue_type, lvalue_type, context, msg,
17111728
'{} has type'.format(rvalue_name),

mypy/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ def add_invertible_flag(flag: str,
222222
help="silently ignore imports of missing modules")
223223
parser.add_argument('--follow-imports', choices=['normal', 'silent', 'skip', 'error'],
224224
default='normal', help="how to treat imports (default normal)")
225+
parser.add_argument('--disallow-implicit-any-types', action='store_true',
226+
help="disallow implicit conversion of types from unanalyzed modules"
227+
" into Any")
225228
add_invertible_flag('--disallow-untyped-calls', default=False, strict_flag=True,
226229
help="disallow calling functions without type annotations"
227230
" from functions with type annotations")

mypy/options.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Options:
1919
PER_MODULE_OPTIONS = {
2020
"ignore_missing_imports",
2121
"follow_imports",
22+
"disallow_implicit_any_types",
2223
"disallow_untyped_calls",
2324
"disallow_untyped_defs",
2425
"check_untyped_defs",
@@ -44,6 +45,7 @@ def __init__(self) -> None:
4445
self.report_dirs = {} # type: Dict[str, str]
4546
self.ignore_missing_imports = False
4647
self.follow_imports = 'normal' # normal|silent|skip|error
48+
self.disallow_implicit_any_types = False
4749

4850
# Disallow calling untyped functions from typed ones
4951
self.disallow_untyped_calls = False
@@ -158,3 +160,6 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:
158160

159161
def select_options_affecting_cache(self) -> Mapping[str, bool]:
160162
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}
163+
164+
def silent_mode(self) -> bool:
165+
return self.ignore_missing_imports or self.follow_imports == 'skip'

mypy/semanal.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
from mypy.nodes import implicit_module_attrs
8686
from mypy.typeanal import (
8787
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
88-
TypeVariableQuery, TypeVarList, remove_dups,
88+
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_silent_import
8989
)
9090
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
9191
from mypy.sametypes import is_same_type
@@ -964,7 +964,9 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
964964
self.fail("Cannot subclass NewType", defn)
965965
base_types.append(base)
966966
elif isinstance(base, AnyType):
967-
if self.options.disallow_subclassing_any:
967+
# if --disallow-implicit-any-types is set, the issue is reported later
968+
if (self.options.disallow_subclassing_any and
969+
not self.options.disallow_implicit_any_types):
968970
if isinstance(base_expr, (NameExpr, MemberExpr)):
969971
msg = "Class cannot subclass '{}' (has type 'Any')".format(base_expr.name)
970972
else:
@@ -974,6 +976,15 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
974976
else:
975977
self.fail('Invalid base class', base_expr)
976978
info.fallback_to_any = True
979+
if (self.options.disallow_implicit_any_types and
980+
has_any_from_silent_import(base)):
981+
if isinstance(base_expr, (NameExpr, MemberExpr)):
982+
msg = ("Subclassing type '{}' that is implicitly converted to '{}' due to "
983+
"import from unanalyzed module".format(base_expr.name, base))
984+
else:
985+
msg = ("Subclassing a type that is implicitly converted to '{}' "
986+
"due to import from unanalyzed module".format(base))
987+
self.fail(msg, base_expr)
977988

978989
# Add 'object' as implicit base if there is no other base class.
979990
if (not base_types and defn.fullname != 'builtins.object'):
@@ -1428,7 +1439,12 @@ def add_unknown_symbol(self, name: str, context: Context, is_import: bool = Fals
14281439
else:
14291440
var._fullname = self.qualified_name(name)
14301441
var.is_ready = True
1431-
var.type = AnyType()
1442+
any_type = AnyType()
1443+
if self.options.silent_mode():
1444+
# if silent mode is not enabled, no need to report implicit conversion to Any,
1445+
# let mypy report an import error.
1446+
any_type.is_from_silent_import = is_import
1447+
var.type = any_type
14321448
var.is_suppressed_import = is_import
14331449
self.add_symbol(name, SymbolTableNode(GDEF, var, self.cur_mod_id), context)
14341450

mypy/typeanal.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
232232
# context. This is slightly problematic as it allows using the type 'Any'
233233
# as a base class -- however, this will fail soon at runtime so the problem
234234
# is pretty minor.
235-
return AnyType()
235+
any_type = AnyType()
236+
any_type.is_from_silent_import = sym.node.type.is_from_silent_import
237+
return any_type
236238
# Allow unbound type variables when defining an alias
237239
if not (self.aliasing and sym.kind == TVAR and
238240
self.tvar_scope.get_binding(sym) is None):
@@ -731,6 +733,23 @@ def visit_callable_type(self, t: CallableType) -> TypeVarList:
731733
return []
732734

733735

736+
def has_any_from_silent_import(t: Type) -> bool:
737+
"""Return true if this type was converted to Any because of a silenced import.
738+
739+
If type t is was co or is has type arguments that contain such Any type
740+
this function will return true.
741+
"""
742+
return t.accept(HasAnyFromSilentImportQuery())
743+
744+
745+
class HasAnyFromSilentImportQuery(TypeQuery[bool]):
746+
def __init__(self) -> None:
747+
super().__init__(any)
748+
749+
def visit_any(self, t: AnyType) -> bool:
750+
return t.is_from_silent_import
751+
752+
734753
def make_optional_type(t: Type) -> Type:
735754
"""Return the type corresponding to Optional[t].
736755

mypy/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ def serialize(self) -> JsonDict:
272272
class AnyType(Type):
273273
"""The type 'Any'."""
274274

275+
# Does this come from a silent import? Used for --disallow-implicit-any-types flag
276+
is_from_silent_import = False
277+
275278
def __init__(self, implicit: bool = False, line: int = -1, column: int = -1) -> None:
276279
super().__init__(line, column)
277280
self.implicit = implicit

test-data/unit/check-flags.test

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,52 @@ if y: # E: Condition must be a boolean
376376
if z: # E: Condition must be a boolean
377377
pass
378378
[builtins fixtures/bool.pyi]
379+
380+
[case testDisallowImplicitTypesIgnoreMissingTypes]
381+
# flags: --ignore-missing-imports --disallow-implicit-any-types
382+
from missing import MyType
383+
384+
def f(x: MyType) -> None: # E: Argument 1 to 'f' is implicitly converted to 'Any' due to import from unanalyzed module
385+
pass
386+
387+
[case testDisallowImplicitTypes]
388+
# flags: --disallow-implicit-any-types
389+
from missing import MyType
390+
391+
def f(x: MyType) -> None:
392+
pass
393+
[out]
394+
main:2: error: Cannot find module named 'missing'
395+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
396+
397+
[case testDisallowImplicitAnyVariableDefinition]
398+
# flags: --ignore-missing-imports --disallow-implicit-any-types
399+
from missing import Unchecked
400+
401+
t: Unchecked = 12 # E: Type of variable is implicitly converted to 'Any' due to import from unanalyzed module
402+
403+
[case testDisallowImplicitAnyGeneric]
404+
# flags: --ignore-missing-imports --disallow-implicit-any-types
405+
from missing import Unchecked
406+
from typing import List
407+
408+
def foo(l: List[Unchecked]) -> List[Unchecked]:
409+
t = [] # type: List[Unchecked]
410+
return l
411+
[builtins fixtures/list.pyi]
412+
[out]
413+
main:5: error: Return type is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module
414+
main:5: error: Argument 1 to 'foo' is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module
415+
main:6: error: Type of variable is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module
416+
417+
[case testDisallowImplicitAnyInherit]
418+
# flags: --ignore-missing-imports --disallow-implicit-any-types
419+
from missing import Unchecked
420+
from typing import List
421+
422+
class C(Unchecked): # E: Subclassing type 'Unchecked' that is implicitly converted to 'Any' due to import from unanalyzed module
423+
pass
424+
425+
class A(List[Unchecked]): # E: Subclassing a type that is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module
426+
pass
427+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)