-
Notifications
You must be signed in to change notification settings - Fork 119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide option to return raw metrics by block type (#192 #194
base: master
Are you sure you want to change the base?
Changes from 5 commits
1c5453b
eccce5f
8983074
b535d0b
99cdf03
322f4fa
35f7d28
22a9bd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
from collections import namedtuple | ||
import ast | ||
import inspect | ||
import sys | ||
|
||
from radon.metrics import analyze | ||
from radon.visitors import ( | ||
GET_ENDLINE, | ||
GET_COMPLEXITY, | ||
GET_REAL_COMPLEXITY, | ||
code2ast | ||
) | ||
from radon.cli.tools import raw_to_dict | ||
|
||
try: | ||
from ast import get_source_segment | ||
except ImportError: | ||
raise ImportError('raw_visitor module requires Python 3.8') | ||
|
||
|
||
|
||
BaseRawFuncMetrics = namedtuple( | ||
'BaseRawFuncMetrics', | ||
[ | ||
'name', | ||
'lineno', | ||
'col_offset', | ||
'endline', | ||
'is_method', | ||
'classname', | ||
'closures', | ||
'loc', | ||
'lloc', | ||
'sloc', | ||
'comments', | ||
'multi', | ||
'blank', | ||
'single_comments', | ||
]) | ||
|
||
BaseRawClassMetrics = namedtuple( | ||
'BaseRawClassMetrics', | ||
[ | ||
'name', | ||
'lineno', | ||
'col_offset', | ||
'endline', | ||
'methods', | ||
'inner_classes', | ||
'loc', | ||
'lloc', | ||
'sloc', | ||
'comments', | ||
'multi', | ||
'blank', | ||
'single_comments', | ||
]) | ||
|
||
|
||
class RawFunctionMetrics(BaseRawFuncMetrics): | ||
'''Object represeting a function block.''' | ||
|
||
@property | ||
def letter(self): | ||
'''The letter representing the function. It is `M` if the function is | ||
actually a method, `F` otherwise. | ||
''' | ||
return 'M' if self.is_method else 'F' | ||
|
||
@property | ||
def fullname(self): | ||
'''The full name of the function. If it is a method, then the full name | ||
is: | ||
{class name}.{method name} | ||
Otherwise it is just the function name. | ||
''' | ||
if self.classname is None: | ||
return self.name | ||
return '{0}.{1}'.format(self.classname, self.name) | ||
|
||
def __str__(self): | ||
'''String representation of a function block.''' | ||
return '{0} {1}:{2}->{3} {4} - sloc: {5}'.format(self.letter, self.lineno, | ||
self.col_offset, | ||
self.endline, | ||
self.fullname, | ||
self.sloc) | ||
|
||
|
||
class RawClassMetrics(BaseRawClassMetrics): | ||
'''Object representing a class block.''' | ||
|
||
letter = 'C' | ||
|
||
@property | ||
def fullname(self): | ||
'''The full name of the class. It is just its name. This attribute | ||
exists for consistency (see :data:`RawFunctionMetrics.fullname`). | ||
''' | ||
return self.name | ||
|
||
def __str__(self): | ||
'''String representation of a class block.''' | ||
return '{0} {1}:{2}->{3} {4} - sloc: {5}'.format(self.letter, self.lineno, | ||
self.col_offset, | ||
self.endline, self.name, | ||
self.sloc) | ||
|
||
|
||
class CodeVisitor(ast.NodeVisitor): | ||
'''Base class for every NodeVisitors in `radon.visitors`. It implements a | ||
couple utility class methods and a static method. | ||
''' | ||
|
||
@staticmethod | ||
def get_name(obj): | ||
'''Shorthand for ``obj.__class__.__name__``.''' | ||
return obj.__class__.__name__ | ||
|
||
@classmethod | ||
def from_code(cls, code, **kwargs): | ||
'''Instantiate the class from source code (string object). The | ||
`**kwargs` are directly passed to the `ast.NodeVisitor` constructor. | ||
''' | ||
cls.code = code | ||
node = code2ast(code) | ||
return cls.from_ast(node, **kwargs) | ||
|
||
@classmethod | ||
def from_ast(cls, ast_node, **kwargs): | ||
'''Instantiate the class from an AST node. The `**kwargs` are | ||
directly passed to the `ast.NodeVisitor` constructor. | ||
''' | ||
visitor = cls(**kwargs) | ||
visitor.visit(ast_node) | ||
return visitor | ||
|
||
|
||
class RawVisitor(CodeVisitor): | ||
'''A visitor that keeps track of raw metrics for block of code. | ||
|
||
Metrics are provided for functions, classes and class methods. | ||
|
||
:param to_method: If True, every function is treated as a method. In this | ||
case the *classname* parameter is used as class name. | ||
:param classname: Name of parent class. | ||
:param off: If True, the starting value for the complexity is set to 1, | ||
otherwise to 0. | ||
''' | ||
|
||
def __init__(self, to_method=False, classname=None): | ||
self.functions = [] | ||
self.classes = [] | ||
self.to_method = to_method | ||
self.classname = classname | ||
self._max_line = float('-inf') | ||
|
||
|
||
@property | ||
def blocks(self): | ||
'''All the blocks visited. These include: all the functions, the | ||
classes and their methods. The returned list is not sorted. | ||
''' | ||
blocks = [] | ||
blocks.extend(self.functions) | ||
for cls in self.classes: | ||
blocks.append(cls) | ||
blocks.extend(cls.methods) | ||
return blocks | ||
|
||
@property | ||
def max_line(self): | ||
'''The maximum line number among the analyzed lines.''' | ||
return self._max_line | ||
|
||
@max_line.setter | ||
def max_line(self, value): | ||
'''The maximum line number among the analyzed lines.''' | ||
if value > self._max_line: | ||
self._max_line = value | ||
|
||
def generic_visit(self, node): | ||
'''Main entry point for the visitor.''' | ||
# Get the name of the class | ||
name = self.get_name(node) | ||
# Check for a lineno attribute | ||
if hasattr(node, 'lineno'): | ||
self.max_line = node.lineno | ||
|
||
super(RawVisitor, self).generic_visit(node) | ||
|
||
def visit_AsyncFunctionDef(self, node): | ||
'''Async function definition is the same thing as the synchronous | ||
one. | ||
''' | ||
self.visit_FunctionDef(node) | ||
|
||
def get_raw_metrics(self, node): | ||
# astunparse.unparse() parses triple quote strings | ||
# a single quote strings. A single quote string is | ||
# interpreted as a sloc instead of a multi. | ||
# source_segement = unparse(node) | ||
|
||
source_segment = get_source_segment(self.code, node) | ||
raw_metrics = analyze(source_segment) | ||
raw_metrics_dict = raw_to_dict(raw_metrics) | ||
self.loc = raw_metrics_dict['loc'] | ||
self.lloc = raw_metrics_dict['lloc'] | ||
self.sloc = raw_metrics_dict['sloc'] | ||
self.comments = raw_metrics_dict['comments'] | ||
self.multi = raw_metrics_dict['multi'] | ||
self.blank = raw_metrics_dict['blank'] | ||
self.single_comments = raw_metrics_dict['single_comments'] | ||
|
||
def visit_FunctionDef(self, node): | ||
'''When visiting functions a new visitor is created to recursively | ||
analyze the function's body. | ||
''' | ||
closures = [] | ||
|
||
for child in node.body: | ||
visitor = RawVisitor() | ||
visitor.visit(child) | ||
closures.extend(visitor.functions) | ||
|
||
self.get_raw_metrics(node) | ||
func_metrics = RawFunctionMetrics(node.name, node.lineno, node.col_offset, | ||
max(node.lineno, visitor.max_line), self.to_method, | ||
self.classname, closures, | ||
self.loc, | ||
self.lloc, | ||
self.sloc, | ||
self.comments, | ||
self.multi, | ||
self.blank, | ||
self.single_comments, | ||
) | ||
|
||
self.functions.append(func_metrics) | ||
|
||
def visit_ClassDef(self, node): | ||
'''When visiting classes a new visitor is created to recursively | ||
analyze the class' body and methods. | ||
''' | ||
methods = [] | ||
classname = node.name | ||
visitors_max_lines = [node.lineno] | ||
inner_classes = [] | ||
for child in node.body: | ||
visitor = RawVisitor( | ||
True, | ||
classname, | ||
) | ||
visitor.visit(child) | ||
methods.extend(visitor.functions) | ||
visitors_max_lines.append(visitor.max_line) | ||
inner_classes.extend(visitor.classes) | ||
|
||
self.get_raw_metrics(node) | ||
cls_metrics = RawClassMetrics(classname, node.lineno, node.col_offset, | ||
max(visitors_max_lines + list(map(GET_ENDLINE, methods))), | ||
methods, inner_classes, | ||
self.loc, | ||
self.lloc, | ||
self.sloc, | ||
self.comments, | ||
self.multi, | ||
self.blank, | ||
self.single_comments, | ||
) | ||
self.classes.append(cls_metrics) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import pytest | ||
|
||
from radon.raw import Module, analyze | ||
from radon.raw_visitor import RawVisitor | ||
from radon.tests import test_raw | ||
|
||
# only testing cases with functions, and remove test with trailing | ||
# comment since this is not in the function scope. | ||
reuseable_tests = test_raw.ANALYZE_CASES[9:13] + test_raw.ANALYZE_CASES[14:] | ||
@pytest.mark.parametrize('code, expected', reuseable_tests) | ||
def test_raw_visitor_functions(code, expected): | ||
code = test_raw.dedent(code) | ||
raw_visitor = RawVisitor.from_code(code) | ||
# only one function in these tests | ||
raw_result = raw_visitor.functions[0] | ||
# exclude the details about function name, lineno, etc. for now | ||
formated_result = Module(*raw_result[7:]) | ||
assert formated_result == Module(*expected), '\n result: \ | ||
{}\n expected: {}'.format(formated_result, Module(*expected)) | ||
assert formated_result.loc == formated_result.blank \ | ||
+ formated_result.sloc \ | ||
+ formated_result.single_comments \ | ||
+ formated_result.multi | ||
|
||
# @pytest.mark.parametrize('code,expected', ANALYZE_CASES) | ||
# def test_analyze(code, expected): | ||
# code = dedent(code) | ||
|
||
# try: | ||
# len(expected) | ||
# except: | ||
# with pytest.raises(expected): | ||
# analyze(code) | ||
# else: | ||
# result = analyze(code) | ||
# assert result == Module(*expected) | ||
# assert result.loc == result.blank + result.sloc + result.single_comments + result.multi | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests need to be enabled so that we know which ones don't pass. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rubik , I suggest deleting these tests since they are not relevant for raw_visitor. The scope of raw.py and raw_visitor.py are different. For example the first couple of tests in test_raw.ANALYZE_CASES include strings and inline code that is not wrapped in a function which should return zeros when metrics are computed from test_raw_visitor.py, but not from raw.py. I wanted to reuse the test cases but I wasn't sure what the best approach was so this was just a quick hack to filter out the reuseable ones. It probably makes sense to separate out the reuseable tests so the current indexing of test_raw.ANALYZE_CASES isn't so fragile as implemented in test_raw_visitor.py. In other words, I could separate test_raw into something like RAW_AND_VISITOR_CASES and RAW_CASES. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BlaneG I see. It makes sense, you can go ahead like you say. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ mando>=0.6,<0.7 | |
colorama==0.4.1 | ||
flake8_polyfill | ||
future | ||
astunparse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be required on Windows, as forward slashes work fine there, as far as I know. But it does break things on Unix platforms, so it has to be reverted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was getting a pytest error with the forward slash:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BlaneG I see. But if we hardcode the separator like that the tests will fail on linux. You can keep the forward-slash and use
os.path.normpath
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
os.path.normpath
? I would personally use thepathlib
API, but that's just me.