diff --git a/mypy/build.py b/mypy/build.py index 6b2e1932ab3b..b2da66ec9904 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -54,6 +54,8 @@ FAST_PARSER = 'fast-parser' # Use experimental fast parser # Disallow calling untyped functions from typed ones DISALLOW_UNTYPED_CALLS = 'disallow-untyped-calls' +# Disallow defining untyped (or incompletely typed) functions +DISALLOW_UNTYPED_DEFS = 'disallow-untyped-defs' # State ids. These describe the states a source file / module can be in a # build. @@ -383,7 +385,8 @@ def __init__(self, data_dir: str, self.type_checker = TypeChecker(self.errors, modules, self.pyversion, - DISALLOW_UNTYPED_CALLS in self.flags) + DISALLOW_UNTYPED_CALLS in self.flags, + DISALLOW_UNTYPED_DEFS in self.flags) self.states = [] # type: List[State] self.module_files = {} # type: Dict[str, str] self.module_deps = {} # type: Dict[Tuple[str, str], bool] diff --git a/mypy/checker.py b/mypy/checker.py index 4df142b88a69..aa707e76597a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -366,10 +366,12 @@ class TypeChecker(NodeVisitor[Type]): current_node_deferred = False # This makes it an error to call an untyped function from a typed one disallow_untyped_calls = False + # This makes it an error to define an untyped or partially-typed function + disallow_untyped_defs = False def __init__(self, errors: Errors, modules: Dict[str, MypyFile], pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, - disallow_untyped_calls=False) -> None: + disallow_untyped_calls=False, disallow_untyped_defs=False) -> None: """Construct a type checker. Use errors to report type check errors. Assume symtable has been @@ -393,6 +395,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], self.pass_num = 0 self.current_node_deferred = False self.disallow_untyped_calls = disallow_untyped_calls + self.disallow_untyped_defs = disallow_untyped_defs def visit_file(self, file_node: MypyFile, path: str) -> None: """Type check a mypy file with the given path.""" @@ -658,6 +661,19 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None: self.fail(messages.INIT_MUST_HAVE_NONE_RETURN_TYPE, item.type) + if self.disallow_untyped_defs: + # Check for functions with unspecified/not fully specified types. + def is_implicit_any(t: Type) -> bool: + return isinstance(t, AnyType) and t.implicit + + if fdef.type is None: + self.fail(messages.FUNCTION_TYPE_EXPECTED, fdef) + elif isinstance(fdef.type, CallableType): + if is_implicit_any(fdef.type.ret_type): + 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 name in nodes.reverse_op_method_set: self.check_reverse_op_method(item, typ, name) elif name == '__getattr__': diff --git a/mypy/main.py b/mypy/main.py index 062593e5ba68..6253ddd56953 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -181,6 +181,9 @@ def process_options(args: List[str]) -> Tuple[List[BuildSource], Options]: elif args[0] == '--disallow-untyped-calls': options.build_flags.append(build.DISALLOW_UNTYPED_CALLS) args = args[1:] + elif args[0] == '--disallow-untyped-defs': + options.build_flags.append(build.DISALLOW_UNTYPED_DEFS) + args = args[1:] elif args[0] in ('--version', '-V'): ver = True args = args[1:] @@ -316,6 +319,8 @@ def usage(msg: str = None) -> None: -s, --silent-imports don't follow imports to .py files --disallow-untyped-calls disallow calling functions without type annotations from functions with type annotations + --disallow-untyped-defs disallow defining functions without type annotations + or with incomplete type annotations --implicit-any behave as though all functions were annotated with Any -f, --dirty-stubs don't warn if typeshed is out of sync --pdb invoke pdb on fatal error diff --git a/mypy/messages.py b/mypy/messages.py index 2c5dcb861ff6..7557fb05bed0 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -73,6 +73,9 @@ RETURN_TYPE_CANNOT_BE_CONTRAVARIANT = "Cannot use a contravariant type variable as return type" FUNCTION_PARAMETER_CANNOT_BE_COVARIANT = "Cannot use a covariant type variable as a parameter" INCOMPATIBLE_IMPORT_OF = "Incompatible import of" +FUNCTION_TYPE_EXPECTED = "Function is missing a type annotation" +RETURN_TYPE_EXPECTED = "Function is missing a return type annotation" +ARGUMENT_TYPE_EXPECTED = "Function is missing a type annotation for one or more arguments" class MessageBuilder: diff --git a/mypy/parse.py b/mypy/parse.py index ed09e18db9d1..3e281275c620 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -477,8 +477,8 @@ def parse_function(self, no_type_checks: bool=False) -> FuncDef: if is_method and name == '__init__': ret_type = UnboundType('None', []) else: - ret_type = AnyType() - typ = CallableType([AnyType() for _ in args], + ret_type = AnyType(implicit=True) + typ = CallableType([AnyType(implicit=True) for _ in args], arg_kinds, [a.variable.name() for a in args], ret_type, @@ -812,9 +812,9 @@ def construct_function_type(self, args: List[Argument], ret_type: Type, arg_types = [arg.type_annotation for arg in args] for i in range(len(arg_types)): if arg_types[i] is None: - arg_types[i] = AnyType() + arg_types[i] = AnyType(implicit=True) if ret_type is None: - ret_type = AnyType() + ret_type = AnyType(implicit=True) arg_kinds = [arg.kind for arg in args] arg_names = [arg.variable.name() for arg in args] return CallableType(arg_types, arg_kinds, arg_names, ret_type, None, name=None, diff --git a/mypy/test/data/check-flags.test b/mypy/test/data/check-flags.test new file mode 100644 index 000000000000..9253d7e3bd4f --- /dev/null +++ b/mypy/test/data/check-flags.test @@ -0,0 +1,32 @@ +# test cases for --disallow-untyped-defs + +[case testUnannotatedFunction] +# flags: disallow-untyped-defs +def f(x): pass +[out] +main: note: In function "f": +main:2: error: Function is missing a type annotation + +[case testUnannotatedArgument] +# flags: disallow-untyped-defs +def f(x) -> int: pass +[out] +main: note: In function "f": +main:2: error: Function is missing a type annotation for one or more arguments + +[case testNoArgumentFunction] +# flags: disallow-untyped-defs +def f() -> int: pass +[out] + +[case testUnannotatedReturn] +# flags: disallow-untyped-defs +def f(x: int): pass +[out] +main: note: In function "f": +main:2: error: Function is missing a return type annotation + +[case testLambda] +# flags: disallow-untyped-defs +lambda x: x +[out] diff --git a/mypy/test/data/lib-stub/builtins.py b/mypy/test/data/lib-stub/builtins.py index 424e317ec20a..29dc26d81091 100644 --- a/mypy/test/data/lib-stub/builtins.py +++ b/mypy/test/data/lib-stub/builtins.py @@ -1,8 +1,10 @@ +class Any: pass + class object: def __init__(self) -> None: pass class type: - def __init__(self, x) -> None: pass + def __init__(self, x: Any) -> None: pass # These are provided here for convenience. class int: pass diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 36992e255d4b..664822fd66aa 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -4,7 +4,7 @@ import re import sys -from typing import Tuple +from typing import Tuple, List from mypy import build import mypy.myunit # for mutable globals (ick!) @@ -53,6 +53,7 @@ 'check-ignore.test', 'check-type-promotion.test', 'check-semanal-error.test', + 'check-flags.test', ] @@ -69,12 +70,13 @@ def run_test(self, testcase): pyversion = testcase_pyversion(testcase.file, testcase.name) program_text = '\n'.join(testcase.input) module_name, program_name, program_text = self.parse_options(program_text) + flags = self.parse_flags(program_text) source = BuildSource(program_name, module_name, program_text) try: build.build(target=build.TYPE_CHECK, sources=[source], pyversion=pyversion, - flags=[build.TEST_BUILTINS], + flags=flags + [build.TEST_BUILTINS], alt_lib_path=test_temp_dir) except CompileError as e: a = normalize_error_messages(e.messages) @@ -109,3 +111,10 @@ def parse_options(self, program_text: str) -> Tuple[str, str, str]: return m.group(1), path, program_text else: return '__main__', 'main', program_text + + def parse_flags(self, program_text: str) -> List[str]: + m = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE) + if m: + return m.group(1).split() + else: + return [] diff --git a/mypy/types.py b/mypy/types.py index d220a1252639..455dcc242116 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -102,6 +102,10 @@ def accept(self, visitor: 'TypeVisitor[T]') -> T: class AnyType(Type): """The type 'Any'.""" + def __init__(self, implicit=False, line: int = -1) -> None: + super().__init__(line) + self.implicit = implicit + def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_any(self)