diff --git a/CHANGES b/CHANGES index 48e22847c..d3ad98ec6 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,7 @@ Bugs fixed * #4789: imgconverter: confused by convert.exe of Windows * #4783: On windows, Sphinx crashed when drives of srcdir and outdir are different +* #4812: autodoc ignores type annotated variables Testing -------- diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index e1ba1a279..6aa8889af 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -23,7 +23,7 @@ from sphinx.util.nodes import explicit_title_re, set_source_info, \ if False: # For type annotation - from typing import Any, Dict, List, Tuple # NOQA + from typing import Any, Dict, Generator, List, Tuple # NOQA from sphinx.application import Sphinx # NOQA diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index f15da664a..deba48b1c 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -12,6 +12,7 @@ import ast import inspect import itertools import re +import sys import tokenize from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING from tokenize import COMMENT, NL @@ -27,6 +28,21 @@ indent_re = re.compile(u'^\\s*$') emptyline_re = re.compile(u'^\\s*(#.*)?$') +if sys.version_info >= (3, 6): + ASSIGN_NODES = (ast.Assign, ast.AnnAssign) +else: + ASSIGN_NODES = (ast.Assign) + + +def get_assign_targets(node): + # type: (ast.AST) -> List[ast.expr] + """Get list of targets from Assign and AnnAssign node.""" + if isinstance(node, ast.Assign): + return node.targets + else: + return [node.target] # type: ignore + + def get_lvar_names(node, self=None): # type: (ast.AST, ast.expr) -> List[unicode] """Convert assignment-AST to variable names. @@ -284,7 +300,8 @@ class VariableCommentPicker(ast.NodeVisitor): # type: (ast.Assign) -> None """Handles Assign node and pick up a variable comment.""" try: - varnames = sum([get_lvar_names(t, self=self.get_self()) for t in node.targets], []) + targets = get_assign_targets(node) + varnames = sum([get_lvar_names(t, self=self.get_self()) for t in targets], []) current_line = self.get_line(node.lineno) except TypeError: return # this assignment is not new definition! @@ -320,12 +337,18 @@ class VariableCommentPicker(ast.NodeVisitor): for varname in varnames: self.add_entry(varname) + def visit_AnnAssign(self, node): + # type: (ast.AST) -> None + """Handles AnnAssign node and pick up a variable comment.""" + self.visit_Assign(node) # type: ignore + def visit_Expr(self, node): # type: (ast.Expr) -> None """Handles Expr node and pick up a comment if string.""" - if (isinstance(self.previous, ast.Assign) and isinstance(node.value, ast.Str)): + if (isinstance(self.previous, ASSIGN_NODES) and isinstance(node.value, ast.Str)): try: - varnames = get_lvar_names(self.previous.targets[0], self.get_self()) + targets = get_assign_targets(self.previous) + varnames = get_lvar_names(targets[0], self.get_self()) for varname in varnames: if isinstance(node.value.s, text_type): docstring = node.value.s diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index b9327999b..09f1f41f5 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for details. """ +import sys + import pytest from six import PY2 @@ -94,6 +96,18 @@ def test_comment_picker_location(): ('Foo', 'attr3'): 'comment for attr3(3)'} +@pytest.mark.skipif(sys.version_info < (3, 6), reason='tests for py36+ syntax') +def test_annotated_assignment_py36(): + source = ('a: str = "Sphinx" #: comment\n' + 'b: int = 1\n' + '"""string on next line"""') + parser = Parser(source) + parser.parse() + assert parser.comments == {('', 'a'): 'comment', + ('', 'b'): 'string on next line'} + assert parser.definitions == {} + + def test_complex_assignment(): source = ('a = 1 + 1; b = a #: compound statement\n' 'c, d = (1, 1) #: unpack assignment\n'