diff options
-rw-r--r-- | sphinx/pycode/__init__.py | 2 | ||||
-rw-r--r-- | sphinx/pycode/parser.py | 38 | ||||
-rw-r--r-- | tests/test_pycode_parser.py | 78 |
3 files changed, 118 insertions, 0 deletions
diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 55d5d2c1d..0a6ff5214 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -144,6 +144,7 @@ class ModuleAnalyzer: # will be filled by parse() self.annotations = None # type: Dict[Tuple[str, str], str] self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] + self.finals = None # type: List[str] self.tagorder = None # type: Dict[str, int] self.tags = None # type: Dict[str, Tuple[str, int, int]] @@ -161,6 +162,7 @@ class ModuleAnalyzer: self.attr_docs[scope] = [''] self.annotations = parser.annotations + self.finals = parser.finals self.tags = parser.definitions self.tagorder = parser.deforders except Exception as exc: diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 3ef1c4605..3762c72cc 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -231,6 +231,9 @@ class VariableCommentPicker(ast.NodeVisitor): self.annotations = {} # type: Dict[Tuple[str, str], str] self.previous = None # type: ast.AST self.deforders = {} # type: Dict[str, int] + self.finals = [] # type: List[str] + self.typing = None # type: str + self.typing_final = None # type: str super().__init__() def get_qualname_for(self, name: str) -> Optional[List[str]]: @@ -249,6 +252,11 @@ class VariableCommentPicker(ast.NodeVisitor): if qualname: self.deforders[".".join(qualname)] = next(self.counter) + def add_final_entry(self, name: str) -> None: + qualname = self.get_qualname_for(name) + if qualname: + self.finals.append(".".join(qualname)) + def add_variable_comment(self, name: str, comment: str) -> None: qualname = self.get_qualname_for(name) if qualname: @@ -261,6 +269,22 @@ class VariableCommentPicker(ast.NodeVisitor): basename = ".".join(qualname[:-1]) self.annotations[(basename, name)] = unparse(annotation) + def is_final(self, decorators: List[ast.expr]) -> bool: + final = [] + if self.typing: + final.append('%s.final' % self.typing) + if self.typing_final: + final.append(self.typing_final) + + for decorator in decorators: + try: + if unparse(decorator) in final: + return True + except NotImplementedError: + pass + + return False + def get_self(self) -> ast.arg: """Returns the name of first argument if in function.""" if self.current_function and self.current_function.args.args: @@ -282,11 +306,19 @@ class VariableCommentPicker(ast.NodeVisitor): for name in node.names: self.add_entry(name.asname or name.name) + if name.name == 'typing': + self.typing = name.asname or name.name + elif name.name == 'typing.final': + self.typing_final = name.asname or name.name + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: """Handles Import node and record it to definition orders.""" for name in node.names: self.add_entry(name.asname or name.name) + if node.module == 'typing' and name.name == 'final': + self.typing_final = name.asname or name.name + def visit_Assign(self, node: ast.Assign) -> None: """Handles Assign node and pick up a variable comment.""" try: @@ -370,6 +402,8 @@ class VariableCommentPicker(ast.NodeVisitor): """Handles ClassDef node and set context.""" self.current_classes.append(node.name) self.add_entry(node.name) + if self.is_final(node.decorator_list): + self.add_final_entry(node.name) self.context.append(node.name) self.previous = node for child in node.body: @@ -381,6 +415,8 @@ class VariableCommentPicker(ast.NodeVisitor): """Handles FunctionDef node and set context.""" if self.current_function is None: self.add_entry(node.name) # should be called before setting self.current_function + if self.is_final(node.decorator_list): + self.add_final_entry(node.name) self.context.append(node.name) self.current_function = node for child in node.body: @@ -481,6 +517,7 @@ class Parser: self.comments = {} # type: Dict[Tuple[str, str], str] self.deforders = {} # type: Dict[str, int] self.definitions = {} # type: Dict[str, Tuple[str, int, int]] + self.finals = [] # type: List[str] def parse(self) -> None: """Parse the source code.""" @@ -495,6 +532,7 @@ class Parser: self.annotations = picker.annotations self.comments = picker.comments self.deforders = picker.deforders + self.finals = picker.finals def parse_definition(self) -> None: """Parse the location of definitions from the code.""" diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index 0bf505a33..398c9f8a4 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -374,3 +374,81 @@ def test_formfeed_char(): parser = Parser(source) parser.parse() assert parser.comments == {('Foo', 'attr'): 'comment'} + + +def test_typing_final(): + source = ('import typing\n' + '\n' + '@typing.final\n' + 'def func(): pass\n' + '\n' + '@typing.final\n' + 'class Foo:\n' + ' @typing.final\n' + ' def meth(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.finals == ['func', 'Foo', 'Foo.meth'] + + +def test_typing_final_from_import(): + source = ('from typing import final\n' + '\n' + '@final\n' + 'def func(): pass\n' + '\n' + '@final\n' + 'class Foo:\n' + ' @final\n' + ' def meth(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.finals == ['func', 'Foo', 'Foo.meth'] + + +def test_typing_final_import_as(): + source = ('import typing as foo\n' + '\n' + '@foo.final\n' + 'def func(): pass\n' + '\n' + '@foo.final\n' + 'class Foo:\n' + ' @typing.final\n' + ' def meth(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.finals == ['func', 'Foo'] + + +def test_typing_final_from_import_as(): + source = ('from typing import final as bar\n' + '\n' + '@bar\n' + 'def func(): pass\n' + '\n' + '@bar\n' + 'class Foo:\n' + ' @final\n' + ' def meth(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.finals == ['func', 'Foo'] + + +def test_typing_final_not_imported(): + source = ('@typing.final\n' + 'def func(): pass\n' + '\n' + '@typing.final\n' + 'class Foo:\n' + ' @final\n' + ' def meth(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.finals == [] |