Skip to content

Disallow implicit Any types from Silent Imports #3405

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

Merged
merged 14 commits into from
Jun 7, 2017
Merged
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
21 changes: 20 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ARG_POS, MDEF,
CONTRAVARIANT, COVARIANT)
from mypy import nodes
from mypy.typeanal import has_any_from_unimported_type
from mypy.types import (
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
Instance, NoneTyp, strip_type, TypeType,
Expand Down Expand Up @@ -611,7 +612,15 @@ def is_implicit_any(t: Type) -> bool:
self.fail(messages.RETURN_TYPE_EXPECTED, fdef)
if any(is_implicit_any(t) for t in fdef.type.arg_types):
self.fail(messages.ARGUMENT_TYPE_EXPECTED, fdef)

if 'unimported' in self.options.disallow_any:
if fdef.type and isinstance(fdef.type, CallableType):
ret_type = fdef.type.ret_type
if has_any_from_unimported_type(ret_type):
self.msg.unimported_type_becomes_any("Return type", ret_type, fdef)
for idx, arg_type in enumerate(fdef.type.arg_types):
if has_any_from_unimported_type(arg_type):
prefix = "Argument {} to \"{}\"".format(idx + 1, fdef.name())
self.msg.unimported_type_becomes_any(prefix, arg_type, fdef)
if name in nodes.reverse_op_method_set:
self.check_reverse_op_method(item, typ, name)
elif name in ('__getattr__', '__getattribute__'):
Expand Down Expand Up @@ -1179,6 +1188,16 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
"""
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)

if (s.type is not None and
'unimported' in self.options.disallow_any and
has_any_from_unimported_type(s.type)):
if isinstance(s.lvalues[-1], TupleExpr):
# This is a multiple assignment. Instead of figuring out which type is problematic,
# give a generic error message.
self.msg.unimported_type_becomes_any("A type on this line", AnyType(), s)
else:
self.msg.unimported_type_becomes_any("Type of variable", s.type, s)

if len(s.lvalues) > 1:
# Chained assignment (e.g. x = y = ...).
# Make sure that rvalue type will not be reinferred.
Expand Down
11 changes: 10 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional

from mypy.errors import report_internal_error
from mypy.typeanal import has_any_from_unimported_type
from mypy.types import (
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
Expand Down Expand Up @@ -1543,8 +1544,11 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
"""Type check a cast expression."""
source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True)
target_type = expr.type
if self.chk.options.warn_redundant_casts and is_same_type(source_type, target_type):
options = self.chk.options
if options.warn_redundant_casts and is_same_type(source_type, target_type):
self.msg.redundant_cast(target_type, expr)
if 'unimported' in options.disallow_any and has_any_from_unimported_type(target_type):
self.msg.unimported_type_becomes_any("Target type of cast", target_type, expr)
return target_type

def visit_reveal_type_expr(self, expr: RevealTypeExpr) -> Type:
Expand Down Expand Up @@ -2229,6 +2233,11 @@ def visit_newtype_expr(self, e: NewTypeExpr) -> Type:
return AnyType()

def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type:
tuple_type = e.info.tuple_type
if tuple_type:
if ('unimported' in self.chk.options.disallow_any and
has_any_from_unimported_type(tuple_type)):
self.msg.unimported_type_becomes_any("NamedTuple type", tuple_type, e)
# TODO: Perhaps return a type object type?
return AnyType()

Expand Down
16 changes: 16 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def process_options(args: List[str],

strict_flag_names = [] # type: List[str]
strict_flag_assignments = [] # type: List[Tuple[str, bool]]
disallow_any_options = ['unimported']

def add_invertible_flag(flag: str,
*,
Expand All @@ -203,6 +204,17 @@ def add_invertible_flag(flag: str,
strict_flag_names.append(flag)
strict_flag_assignments.append((dest, not default))

def disallow_any_argument_type(raw_options: str) -> List[str]:
flag_options = raw_options.split(',')
for option in flag_options:
if option not in disallow_any_options:
formatted_valid_options = ', '.join(
"'{}'".format(o) for o in disallow_any_options)
message = "Invalid '--disallow-any' option '{}' (valid options are: {}).".format(
option, formatted_valid_options)
raise argparse.ArgumentError(None, message)
return flag_options

# Unless otherwise specified, arguments will be parsed directly onto an
# Options object. Options that require further processing should have
# their `dest` prefixed with `special-opts:`, which will cause them to be
Expand All @@ -222,6 +234,10 @@ def add_invertible_flag(flag: str,
help="silently ignore imports of missing modules")
parser.add_argument('--follow-imports', choices=['normal', 'silent', 'skip', 'error'],
default='normal', help="how to treat imports (default normal)")
parser.add_argument('--disallow-any', type=disallow_any_argument_type, default=[],
metavar='{{{}}}'.format(', '.join(disallow_any_options)),
help="disallow various types of Any in a module. Takes a comma-separated "
"list of options (defaults to all options disabled)")
add_invertible_flag('--disallow-untyped-calls', default=False, strict_flag=True,
help="disallow calling functions without type annotations"
" from functions with type annotations")
Expand Down
4 changes: 4 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,10 @@ def unsupported_type_type(self, item: Type, context: Context) -> None:
def redundant_cast(self, typ: Type, context: Context) -> None:
self.note('Redundant cast to {}'.format(self.format(typ)), context)

def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> None:
self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)),
ctx)

def typeddict_instantiated_with_unexpected_items(self,
expected_item_names: List[str],
actual_item_names: List[str],
Expand Down
4 changes: 3 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pprint
import sys

from typing import Any, Mapping, Optional, Tuple, List, Pattern, Dict
from typing import Mapping, Optional, Tuple, List, Pattern, Dict

from mypy import defaults

Expand All @@ -19,6 +19,7 @@ class Options:
PER_MODULE_OPTIONS = {
"ignore_missing_imports",
"follow_imports",
"disallow_any",
"disallow_untyped_calls",
"disallow_untyped_defs",
"check_untyped_defs",
Expand All @@ -44,6 +45,7 @@ def __init__(self) -> None:
self.report_dirs = {} # type: Dict[str, str]
self.ignore_missing_imports = False
self.follow_imports = 'normal' # normal|silent|skip|error
self.disallow_any = [] # type: List[str]

# Disallow calling untyped functions from typed ones
self.disallow_untyped_calls = False
Expand Down
23 changes: 20 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
from mypy.visitor import NodeVisitor
from mypy.traverser import TraverserVisitor
from mypy.errors import Errors, report_internal_error
from mypy.messages import CANNOT_ASSIGN_TO_TYPE
from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder
from mypy.types import (
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType,
Expand All @@ -85,7 +85,7 @@
from mypy.nodes import implicit_module_attrs
from mypy.typeanal import (
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
TypeVariableQuery, TypeVarList, remove_dups,
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type
)
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
from mypy.sametypes import is_same_type
Expand Down Expand Up @@ -236,6 +236,7 @@ def __init__(self,
self.lib_path = lib_path
self.errors = errors
self.modules = modules
self.msg = MessageBuilder(errors, modules)
self.missing_modules = missing_modules
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
self.postponed_functions_stack = []
Expand Down Expand Up @@ -974,6 +975,12 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
else:
self.fail('Invalid base class', base_expr)
info.fallback_to_any = True
if 'unimported' in self.options.disallow_any and has_any_from_unimported_type(base):
if isinstance(base_expr, (NameExpr, MemberExpr)):
prefix = "Base type {}".format(base_expr.name)
else:
prefix = "Base type"
self.msg.unimported_type_becomes_any(prefix, base, base_expr)

# Add 'object' as implicit base if there is no other base class.
if (not base_types and defn.fullname != 'builtins.object'):
Expand Down Expand Up @@ -1428,7 +1435,7 @@ def add_unknown_symbol(self, name: str, context: Context, is_import: bool = Fals
else:
var._fullname = self.qualified_name(name)
var.is_ready = True
var.type = AnyType()
var.type = AnyType(from_unimported_type=is_import)
var.is_suppressed_import = is_import
self.add_symbol(name, SymbolTableNode(GDEF, var, self.cur_mod_id), context)

Expand Down Expand Up @@ -1875,6 +1882,16 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> None:
return
variance, upper_bound = res

if 'unimported' in self.options.disallow_any:
for idx, constraint in enumerate(values, start=1):
if has_any_from_unimported_type(constraint):
prefix = "Constraint {}".format(idx)
self.msg.unimported_type_becomes_any(prefix, constraint, s)

if has_any_from_unimported_type(upper_bound):
prefix = "Upper bound of type variable"
self.msg.unimported_type_becomes_any(prefix, upper_bound, s)

# Yes, it's a valid type variable definition! Add it to the symbol table.
node = self.lookup(name, s)
node.kind = TVAR
Expand Down
19 changes: 18 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
# context. This is slightly problematic as it allows using the type 'Any'
# as a base class -- however, this will fail soon at runtime so the problem
# is pretty minor.
return AnyType()
return AnyType(from_unimported_type=True)
# Allow unbound type variables when defining an alias
if not (self.aliasing and sym.kind == TVAR and
self.tvar_scope.get_binding(sym) is None):
Expand Down Expand Up @@ -731,6 +731,23 @@ def visit_callable_type(self, t: CallableType) -> TypeVarList:
return []


def has_any_from_unimported_type(t: Type) -> bool:
"""Return true if this type is Any because an import was not followed.

If type t is such Any type or has type arguments that contain such Any type
this function will return true.
"""
return t.accept(HasAnyFromUnimportedType())


class HasAnyFromUnimportedType(TypeQuery[bool]):
def __init__(self) -> None:
super().__init__(any)

def visit_any(self, t: AnyType) -> bool:
return t.from_unimported_type


def make_optional_type(t: Type) -> Type:
"""Return the type corresponding to Optional[t].

Expand Down
9 changes: 8 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,16 @@ def serialize(self) -> JsonDict:
class AnyType(Type):
"""The type 'Any'."""

def __init__(self, implicit: bool = False, line: int = -1, column: int = -1) -> None:
def __init__(self,
implicit: bool = False,
from_unimported_type: bool = False,
line: int = -1,
column: int = -1) -> None:
super().__init__(line, column)
# Was this Any type was inferred without a type annotation?
self.implicit = implicit
# Does this come from an unfollowed import? See --disallow-any=unimported option
self.from_unimported_type = from_unimported_type

def accept(self, visitor: 'TypeVisitor[T]') -> T:
return visitor.visit_any(self)
Expand Down
Loading