diff --git a/mypy/build.py b/mypy/build.py index 21fde0239c16..209a0cb37537 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -638,7 +638,7 @@ def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> My Raise CompileError if there is a parse error. """ num_errs = self.errors.num_messages() - tree = parse(source, path, id, self.errors, options=self.options) + tree = parse(source, path, id, self.errors, options=self.options, plugin=self.plugin) tree._fullname = id self.add_stats(files_parsed=1, modules_parsed=int(not tree.is_stub), diff --git a/mypy/fastparse.py b/mypy/fastparse.py index b006c3da7e88..688cd9b20928 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -33,6 +33,7 @@ from mypy import messages from mypy.errors import Errors from mypy.options import Options +from mypy.plugin import Plugin, DocstringParserContext, TypeMap try: from typed_ast import ast3 @@ -64,13 +65,16 @@ TYPE_COMMENT_SYNTAX_ERROR = 'syntax error in type comment' TYPE_COMMENT_AST_ERROR = 'invalid type comment or annotation' +TYPE_COMMENT_DOCSTRING_ERROR = ('One or more arguments specified in docstring are not ' + 'present in function signature: {}') def parse(source: Union[str, bytes], fnam: str, module: Optional[str], errors: Optional[Errors] = None, - options: Optional[Options] = None) -> MypyFile: + options: Optional[Options] = None, + plugin: Optional[Plugin] = None) -> MypyFile: """Parse a source file, without doing any semantic analysis. @@ -83,6 +87,8 @@ def parse(source: Union[str, bytes], raise_on_error = True if options is None: options = Options() + if plugin is None: + plugin = Plugin(options) errors.set_file(fnam, module) is_stub_file = fnam.endswith('.pyi') try: @@ -96,6 +102,7 @@ def parse(source: Union[str, bytes], tree = ASTConverter(options=options, is_stub=is_stub_file, errors=errors, + plugin=plugin, ).visit(ast) tree.path = fnam tree.is_stub = is_stub_file @@ -123,6 +130,25 @@ def parse_type_comment(type_comment: str, line: int, errors: Optional[Errors]) - return TypeConverter(errors, line=line).visit(typ.body) +def parse_docstring(hook: Callable[[DocstringParserContext], TypeMap], docstring: str, + arg_names: List[str], line: int, errors: Errors + ) -> Optional[Tuple[List[Type], Type]]: + """Parse a docstring and return type representations. + + Returns a 2-tuple: (list of arguments Types, and return Type). + """ + type_map = hook(DocstringParserContext(docstring, line, errors)) + if type_map: + arg_types = [type_map.pop(name, AnyType(TypeOfAny.unannotated)) + for name in arg_names] + return_type = type_map.pop('return', AnyType(TypeOfAny.unannotated)) + if type_map: + errors.report(line, 0, + TYPE_COMMENT_DOCSTRING_ERROR.format(list(type_map))) + return arg_types, return_type + return None + + def with_line(f: Callable[['ASTConverter', T], U]) -> Callable[['ASTConverter', T], U]: @wraps(f) def wrapper(self: 'ASTConverter', ast: T) -> U: @@ -152,13 +178,15 @@ class ASTConverter(ast3.NodeTransformer): def __init__(self, options: Options, is_stub: bool, - errors: Errors) -> None: + errors: Errors, + plugin: Plugin) -> None: self.class_nesting = 0 self.imports = [] # type: List[ImportBase] self.options = options self.is_stub = is_stub self.errors = errors + self.plugin = plugin def note(self, msg: str, line: int, column: int) -> None: self.errors.report(line, column, msg, severity='note') @@ -324,53 +352,72 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef], args = self.transform_args(n.args, n.lineno, no_type_check=no_type_check) arg_kinds = [arg.kind for arg in args] - arg_names = [arg.variable.name() for arg in args] # type: List[Optional[str]] - arg_names = [None if argument_elide_name(name) else name for name in arg_names] + real_names = [arg.variable.name() for arg in args] # type: List[str] + arg_names = [None if argument_elide_name(name) else name + for name in real_names] # type: List[Optional[str]] if special_function_elide_names(n.name): arg_names = [None] * len(arg_names) arg_types = [] # type: List[Optional[Type]] if no_type_check: arg_types = [None] * len(args) return_type = None - elif n.type_comment is not None: - try: - func_type_ast = ast3.parse(n.type_comment, '', 'func_type') - assert isinstance(func_type_ast, ast3.FunctionType) - # for ellipsis arg - if (len(func_type_ast.argtypes) == 1 and - isinstance(func_type_ast.argtypes[0], ast3.Ellipsis)): - if n.returns: - # PEP 484 disallows both type annotations and type comments - self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) - arg_types = [a.type_annotation - if a.type_annotation is not None - else AnyType(TypeOfAny.unannotated) - for a in args] - else: - # PEP 484 disallows both type annotations and type comments - if n.returns or any(a.type_annotation is not None for a in args): - self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) - translated_args = (TypeConverter(self.errors, line=n.lineno) - .translate_expr_list(func_type_ast.argtypes)) - arg_types = [a if a is not None else AnyType(TypeOfAny.unannotated) - for a in translated_args] - return_type = TypeConverter(self.errors, - line=n.lineno).visit(func_type_ast.returns) - - # add implicit self type - if self.in_class() and len(arg_types) < len(args): - arg_types.insert(0, AnyType(TypeOfAny.special_form)) - except SyntaxError: - self.fail(TYPE_COMMENT_SYNTAX_ERROR, n.lineno, n.col_offset) - if n.type_comment and n.type_comment[0] != "(": - self.note('Suggestion: wrap argument types in parentheses', - n.lineno, n.col_offset) - arg_types = [AnyType(TypeOfAny.from_error)] * len(args) - return_type = AnyType(TypeOfAny.from_error) else: - arg_types = [a.type_annotation for a in args] - return_type = TypeConverter(self.errors, line=n.returns.lineno - if n.returns else n.lineno).visit(n.returns) + doc_types = None # type: Optional[Tuple[List[Type], Type]] + docstring_hook = self.plugin.get_docstring_parser_hook() + if docstring_hook is not None: + doc = ast3.get_docstring(n, clean=False) + if doc: + doc_types = parse_docstring(docstring_hook, doc, real_names, n.lineno, + self.errors) + + if n.type_comment is not None: + if doc_types is not None: + # PEP 484 disallows both type annotations and type comments + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + try: + func_type_ast = ast3.parse(n.type_comment, '', 'func_type') + assert isinstance(func_type_ast, ast3.FunctionType) + # for ellipsis arg + if (len(func_type_ast.argtypes) == 1 and + isinstance(func_type_ast.argtypes[0], ast3.Ellipsis)): + if n.returns: + # PEP 484 disallows both type annotations and type comments + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + arg_types = [a.type_annotation + if a.type_annotation is not None + else AnyType(TypeOfAny.unannotated) + for a in args] + else: + # PEP 484 disallows both type annotations and type comments + if n.returns or any(a.type_annotation is not None for a in args): + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + translated_args = (TypeConverter(self.errors, line=n.lineno) + .translate_expr_list(func_type_ast.argtypes)) + arg_types = [a if a is not None else AnyType(TypeOfAny.unannotated) + for a in translated_args] + return_type = TypeConverter(self.errors, + line=n.lineno).visit(func_type_ast.returns) + + # add implicit self type + if self.in_class() and len(arg_types) < len(args): + arg_types.insert(0, AnyType(TypeOfAny.special_form)) + except SyntaxError: + self.fail(TYPE_COMMENT_SYNTAX_ERROR, n.lineno, n.col_offset) + if n.type_comment and n.type_comment[0] != "(": + self.note('Suggestion: wrap argument types in parentheses', + n.lineno, n.col_offset) + arg_types = [AnyType(TypeOfAny.from_error)] * len(args) + return_type = AnyType(TypeOfAny.from_error) + elif doc_types is not None: + # PEP 484 disallows both type annotations and type comments + if (n.type_comment is not None or n.returns or + any(a.type_annotation is not None for a in args)): + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + arg_types, return_type = doc_types + else: + arg_types = [a.type_annotation for a in args] + return_type = TypeConverter(self.errors, line=n.returns.lineno + if n.returns else n.lineno).visit(n.returns) for arg, arg_type in zip(args, arg_types): self.set_type_optional(arg_type, arg.initializer) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 66753056b996..6ebcc339d77e 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -41,8 +41,9 @@ from mypy import experiments from mypy import messages from mypy.errors import Errors -from mypy.fastparse import TypeConverter, parse_type_comment +from mypy.fastparse import TypeConverter, parse_type_comment, parse_docstring from mypy.options import Options +from mypy.plugin import Plugin try: from typed_ast import ast27 @@ -81,7 +82,8 @@ def parse(source: Union[str, bytes], fnam: str, module: Optional[str], errors: Optional[Errors] = None, - options: Optional[Options] = None) -> MypyFile: + options: Optional[Options] = None, + plugin: Optional[Plugin] = None) -> MypyFile: """Parse a source file, without doing any semantic analysis. Return the parse tree. If errors is not provided, raise ParseError @@ -93,6 +95,8 @@ def parse(source: Union[str, bytes], raise_on_error = True if options is None: options = Options() + if plugin is None: + plugin = Plugin(options) errors.set_file(fnam, module) is_stub_file = fnam.endswith('.pyi') try: @@ -101,6 +105,7 @@ def parse(source: Union[str, bytes], tree = ASTConverter(options=options, is_stub=is_stub_file, errors=errors, + plugin=plugin, ).visit(ast) assert isinstance(tree, MypyFile) tree.path = fnam @@ -144,13 +149,15 @@ class ASTConverter(ast27.NodeTransformer): def __init__(self, options: Options, is_stub: bool, - errors: Errors) -> None: + errors: Errors, + plugin: Plugin) -> None: self.class_nesting = 0 self.imports = [] # type: List[ImportBase] self.options = options self.is_stub = is_stub self.errors = errors + self.plugin = plugin def fail(self, msg: str, line: int, column: int) -> None: self.errors.report(line, column, msg, blocker=True) @@ -301,8 +308,9 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement: args, decompose_stmts = self.transform_args(n.args, n.lineno) arg_kinds = [arg.kind for arg in args] - arg_names = [arg.variable.name() for arg in args] # type: List[Optional[str]] - arg_names = [None if argument_elide_name(name) else name for name in arg_names] + real_names = [arg.variable.name() for arg in args] # type: List[str] + arg_names = [None if argument_elide_name(name) else name + for name in real_names] # type: List[Optional[str]] if special_function_elide_names(n.name): arg_names = [None] * len(arg_names) @@ -310,35 +318,52 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement: if (n.decorator_list and any(is_no_type_check_decorator(d) for d in n.decorator_list)): arg_types = [None] * len(args) return_type = None - elif n.type_comment is not None and len(n.type_comment) > 0: - try: - func_type_ast = ast3.parse(n.type_comment, '', 'func_type') - assert isinstance(func_type_ast, ast3.FunctionType) - # for ellipsis arg - if (len(func_type_ast.argtypes) == 1 and - isinstance(func_type_ast.argtypes[0], ast3.Ellipsis)): - arg_types = [a.type_annotation - if a.type_annotation is not None - else AnyType(TypeOfAny.unannotated) - for a in args] - else: - # PEP 484 disallows both type annotations and type comments - if any(a.type_annotation is not None for a in args): - self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) - arg_types = [a if a is not None else AnyType(TypeOfAny.unannotated) for - a in converter.translate_expr_list(func_type_ast.argtypes)] - return_type = converter.visit(func_type_ast.returns) - - # add implicit self type - if self.in_class() and len(arg_types) < len(args): - arg_types.insert(0, AnyType(TypeOfAny.special_form)) - except SyntaxError: - self.fail(TYPE_COMMENT_SYNTAX_ERROR, n.lineno, n.col_offset) - arg_types = [AnyType(TypeOfAny.from_error)] * len(args) - return_type = AnyType(TypeOfAny.from_error) else: - arg_types = [a.type_annotation for a in args] - return_type = converter.visit(None) + doc_types = None # type: Optional[Tuple[List[Type], Type]] + docstring_hook = self.plugin.get_docstring_parser_hook() + if docstring_hook is not None: + doc = ast27.get_docstring(n, clean=False) + if doc: + doc_types = parse_docstring(docstring_hook, doc.decode('unicode-escape'), + real_names, n.lineno, self.errors) + + if n.type_comment is not None and len(n.type_comment) > 0: + if doc_types is not None: + # PEP 484 disallows both type annotations and type comments + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + try: + func_type_ast = ast3.parse(n.type_comment, '', 'func_type') + assert isinstance(func_type_ast, ast3.FunctionType) + # for ellipsis arg + if (len(func_type_ast.argtypes) == 1 and + isinstance(func_type_ast.argtypes[0], ast3.Ellipsis)): + arg_types = [a.type_annotation + if a.type_annotation is not None + else AnyType(TypeOfAny.unannotated) + for a in args] + else: + # PEP 484 disallows both type annotations and type comments + if any(a.type_annotation is not None for a in args): + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + arg_types = [a if a is not None else AnyType(TypeOfAny.unannotated) for + a in converter.translate_expr_list(func_type_ast.argtypes)] + return_type = converter.visit(func_type_ast.returns) + + # add implicit self type + if self.in_class() and len(arg_types) < len(args): + arg_types.insert(0, AnyType(TypeOfAny.special_form)) + except SyntaxError: + self.fail(TYPE_COMMENT_SYNTAX_ERROR, n.lineno, n.col_offset) + arg_types = [AnyType(TypeOfAny.from_error)] * len(args) + return_type = AnyType(TypeOfAny.from_error) + elif doc_types is not None: + # PEP 484 disallows both type annotations and type comments + if any(a.type_annotation is not None for a in args): + self.fail(messages.DUPLICATE_TYPE_SIGNATURES, n.lineno, n.col_offset) + arg_types, return_type = doc_types + else: + arg_types = [a.type_annotation for a in args] + return_type = converter.visit(None) for arg, arg_type in zip(args, arg_types): self.set_type_optional(arg_type, arg.initializer) diff --git a/mypy/parse.py b/mypy/parse.py index d845138727c2..cdfadde25dfb 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -2,6 +2,7 @@ from mypy.errors import Errors from mypy.options import Options +from mypy.plugin import Plugin from mypy.nodes import MypyFile @@ -9,7 +10,8 @@ def parse(source: Union[str, bytes], fnam: str, module: Optional[str], errors: Optional[Errors], - options: Options) -> MypyFile: + options: Options, + plugin: Optional[Plugin]) -> MypyFile: """Parse a source file, without doing any semantic analysis. Return the parse tree. If errors is not provided, raise ParseError @@ -24,11 +26,13 @@ def parse(source: Union[str, bytes], fnam=fnam, module=module, errors=errors, - options=options) + options=options, + plugin=plugin) else: import mypy.fastparse2 return mypy.fastparse2.parse(source, fnam=fnam, module=module, errors=errors, - options=options) + options=options, + plugin=plugin) diff --git a/mypy/plugin.py b/mypy/plugin.py index 4ffa9395afc5..ff70a7111c26 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,7 +2,7 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar +from typing import Callable, Dict, List, Tuple, Optional, NamedTuple, TypeVar from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef from mypy.types import ( @@ -12,6 +12,10 @@ from mypy.messages import MessageBuilder from mypy.options import Options +# Can't use TYPE_CHECKING because it's not in the Python 3.5.1 stdlib +MYPY = False +if MYPY: + from mypy.errors import Errors class TypeAnalyzerPluginInterface: """Interface for accessing semantic analyzer functionality in plugins.""" @@ -123,6 +127,24 @@ def fail(self, msg: str, ctx: Context, serious: bool = False, *, ('api', SemanticAnalyzerPluginInterface) ]) +# A context for a hook that extracts type annotations from docstrings. +# +# Called for each unannotated function that has a docstring. +# The function's return type, if specified, is stored in the mapping with the special +# key 'return'. Other than 'return', each key of the mapping must be one of the +# arguments of the documented function; otherwise, an error will be raised. +DocstringParserContext = NamedTuple( + 'DocstringParserContext', [ + ('docstring', str), # The docstring to be parsed + ('line', int), # The line number where the docstring begins + ('errors', 'Errors') # Errors object for reporting errors, warnings, and info + ]) + +TypeMap = Dict[ + str, # Argument name, or 'return' for return type. + Type # Corresponding type extracted from docstring +] + class Plugin: """Base class of all type checker plugins. @@ -173,6 +195,10 @@ def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return None + def get_docstring_parser_hook(self + ) -> Optional[Callable[[DocstringParserContext], TypeMap]]: + return None + T = TypeVar('T') @@ -229,6 +255,10 @@ def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname)) + def get_docstring_parser_hook(self + ) -> Optional[Callable[[DocstringParserContext], TypeMap]]: + return self._find_hook(lambda plugin: plugin.get_docstring_parser_hook()) + def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: for plugin in self._plugins: hook = lookup(plugin) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 707fb32f06d3..fc2c62d1e1ca 100644 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -67,6 +67,7 @@ from mypy.options import Options as MypyOptions from mypy.types import Type, TypeStrVisitor, AnyType, CallableType, UnboundType, NoneTyp, TupleType from mypy.visitor import NodeVisitor +from mypy.plugin import Plugin Options = NamedTuple('Options', [('pyversion', Tuple[int, int]), ('no_import', bool), @@ -204,8 +205,9 @@ def generate_stub(path: str, source = f.read() options = MypyOptions() options.python_version = pyversion + plugin = Plugin(options) try: - ast = mypy.parse.parse(source, fnam=path, module=module, errors=None, options=options) + ast = mypy.parse.parse(source, fnam=path, module=module, errors=None, options=options, plugin=plugin) except mypy.errors.CompileError as e: # Syntax error! for m in e.messages: diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index b33dfba6405f..96a2b701466f 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -76,6 +76,7 @@ 'check-incomplete-fixture.test', 'check-custom-plugin.test', 'check-default-plugin.test', + 'check-docstring-hook.test', ] diff --git a/mypy/test/testparse.py b/mypy/test/testparse.py index 17546d87e682..99014bed64e9 100644 --- a/mypy/test/testparse.py +++ b/mypy/test/testparse.py @@ -8,6 +8,7 @@ from mypy.parse import parse from mypy.errors import CompileError from mypy.options import Options +from mypy.plugin import Plugin class ParserSuite(DataSuite): @@ -35,7 +36,8 @@ def test_parser(testcase: DataDrivenTestCase) -> None: fnam='main', module='__main__', errors=None, - options=options) + options=options, + plugin=Plugin(options)) a = str(n).split('\n') except CompileError as e: a = e.messages @@ -59,8 +61,9 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: def test_parse_error(testcase: DataDrivenTestCase) -> None: try: # Compile temporary file. The test file contains non-ASCII characters. + options = Options() parse(bytes('\n'.join(testcase.input), 'utf-8'), INPUT_FILE_NAME, '__main__', None, - Options()) + options, Plugin(options)) raise AssertionFailure('No errors reported') except CompileError as e: assert e.module_with_blocker == '__main__' diff --git a/test-data/unit/check-docstring-hook.test b/test-data/unit/check-docstring-hook.test new file mode 100644 index 000000000000..cbb1d0fbd627 --- /dev/null +++ b/test-data/unit/check-docstring-hook.test @@ -0,0 +1,169 @@ +[case testFunctionDocstringHook] +# flags: --config-file tmp/mypy.ini +def f(x): + """ + x: int + return: str + """ + return 1 # E: Incompatible return value type (got "int", expected "str") +f('') # E: Argument 1 to "f" has incompatible type "str"; expected "int" +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py + +[case testMethodDocstringHook] +# flags: --config-file tmp/mypy.ini +class A: + def f(self, x): + """ + x: int + return: str + """ + self.f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int" + return 1 # E: Incompatible return value type (got "int", expected "str") +A().f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int" +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py + +[case testSparseDocstringAnnotations] +# flags: --config-file tmp/mypy.ini +def f(x, y): + """ + x: int + """ + return 1 +f('', 1) # E: Argument 1 to "f" has incompatible type "str"; expected "int" +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py + +[case testInvalidDocstringAnnotation] +# flags: --config-file tmp/mypy.ini +def f(x): + """ + x: B/A/D + return: None + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:3: error: invalid type comment or annotation + +[case testDuplicateTypeSignaturesAnnotations] +# flags: --config-file tmp/mypy.ini +def f(x: int): + """ + x: int + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:2: error: Function has duplicate type signatures + +[case testDuplicateTypeSignaturesComments] +# flags: --config-file tmp/mypy.ini +def f(x): + # type: (int) -> None + """ + x: int + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:2: error: Function has duplicate type signatures + +[case testDuplicateTypeSignaturesMultiComments] +# flags: --config-file tmp/mypy.ini +def f(x # type: int + ): + # type: (...) -> None + """ + x: int + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:2: error: Function has duplicate type signatures + +-- Python 2.7 +-- ------------------------- + +[case testFunctionDocstringHook27] +# flags: --config-file tmp/mypy.ini --python-version 2.7 +def f(x): + """ + x: int + return: str + """ + return 1 # E: Incompatible return value type (got "int", expected "str") +f('') # E: Argument 1 to "f" has incompatible type "str"; expected "int" +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py + +[case testMethodDocstringHook27] +# flags: --config-file tmp/mypy.ini --python-version 2.7 +class A: + def f(self, x): + """ + x: int + return: str + """ + self.f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int" + return 1 # E: Incompatible return value type (got "int", expected "str") +A().f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int" +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py + +[case testInvalidDocstringAnnotation27] +# flags: --config-file tmp/mypy.ini --python-version 2.7 +def f(x): + """ + x: B/A/D + return: None + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:3: error: invalid type comment or annotation + +[case testDuplicateTypeSignaturesComments27] +# flags: --config-file tmp/mypy.ini --python-version 2.7 +def f(x): + # type: (int) -> None + """ + x: int + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:2: error: Function has duplicate type signatures + +[case testDuplicateTypeSignaturesMultiComments27] +# flags: --config-file tmp/mypy.ini --python-version 2.7 +def f(x # type: int + ): + # type: (...) -> None + """ + x: int + """ + return None +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/docstring.py +[out] +main:2: error: Function has duplicate type signatures diff --git a/test-data/unit/plugins/docstring.py b/test-data/unit/plugins/docstring.py new file mode 100644 index 000000000000..a8bc81bb1bca --- /dev/null +++ b/test-data/unit/plugins/docstring.py @@ -0,0 +1,14 @@ +from mypy.plugin import Plugin +from mypy.fastparse import parse_type_comment + +class MyPlugin(Plugin): + def get_docstring_parser_hook(self): + return my_hook + +def my_hook(ctx): + params = [l.split(':', 1) for l in ctx.docstring.strip().split('\n')] + return {k.strip(): parse_type_comment(v.strip(), ctx.line + i + 1, ctx.errors) + for i, (k, v) in enumerate(params)} + +def plugin(version): + return MyPlugin