mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
pycode: Detect @final decorators
This commit is contained in:
parent
f388114d10
commit
9c98b92c6a
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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 == []
|
||||
|
Loading…
Reference in New Issue
Block a user