pycode: Detect @final decorators

This commit is contained in:
Takeshi KOMIYA 2020-04-26 21:42:55 +09:00
parent f388114d10
commit 9c98b92c6a
3 changed files with 118 additions and 0 deletions

View File

@ -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:

View File

@ -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."""

View File

@ -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 == []