From 1c5453b45140cf132d05b14f63de2ad9e955d6e5 Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Sun, 28 Jun 2020 10:46:19 -0700 Subject: [PATCH 1/8] feat(raw_visitor): Implemented basic RawVisitor on top of ComplexityVisitor. This is a first commit to implement RawVisitor on top of ComplexityVisitor. Now I have to remove implementations details related to the ComplexityVisitor. --- radon/raw_visitor.py | 329 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 1 + 3 files changed, 331 insertions(+) create mode 100644 radon/raw_visitor.py diff --git a/radon/raw_visitor.py b/radon/raw_visitor.py new file mode 100644 index 0000000..6159d7f --- /dev/null +++ b/radon/raw_visitor.py @@ -0,0 +1,329 @@ +from collections import namedtuple +import ast + +from astunparse import unparse + +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 + + +BaseRawFuncMetrics = namedtuple( + 'BaseRawFuncMetrics', + [ + 'name', + 'lineno', + 'col_offset', + 'endline', + 'is_method', + 'classname', + 'closures', + 'complexity', + 'loc', + 'lloc', + 'sloc', + 'comments', + 'multi', + 'blank', + 'single_comments', + ]) + +BaseRawClassMetrics = namedtuple( + 'BaseRawClassMetrics', + [ + 'name', + 'lineno', + 'col_offset', + 'endline', + 'methods', + 'inner_classes', + 'real_complexity', + 'loc', + 'lloc', + 'sloc', + 'comments', + 'multi', + 'blank', + 'single_comments', + ]) + + +class RawFunction(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} - {5}'.format(self.letter, self.lineno, + self.col_offset, + self.endline, + self.fullname, + self.complexity) + + +class RawClass(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:`RawFunction.fullname`). + ''' + return self.name + + @property + def complexity(self): + '''The average complexity of the class. It corresponds to the average + complexity of its methods plus one. + ''' + if not self.methods: + return self.real_complexity + methods = len(self.methods) + return int(self.real_complexity / float(methods)) + (methods > 1) + + def __str__(self): + '''String representation of a class block.''' + return '{0} {1}:{2}->{3} {4} - {5}'.format(self.letter, self.lineno, + self.col_offset, + self.endline, self.name, + self.complexity) + + +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): + '''Instanciate the class from source code (string object). The + `**kwargs` are directly passed to the `ast.NodeVisitor` constructor. + ''' + return cls.from_ast(code2ast(code), **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 the cyclomatic complexity of + the elements. + + :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, off=True, + no_assert=False): + self.off = off + self.complexity = 1 if off else 0 + self.functions = [] + self.classes = [] + self.to_method = to_method + self.classname = classname + self.no_assert = no_assert + self._max_line = float('-inf') + + @property + def functions_complexity(self): + '''The total complexity from all functions (i.e. the total number of + decision points + 1). + + This is *not* the sum of all the complexity from the functions. Rather, + it's the complexity of the code *inside* all the functions. + ''' + return sum(map(GET_COMPLEXITY, self.functions)) - len(self.functions) + + @property + def classes_complexity(self): + '''The total complexity from all classes (i.e. the total number of + decision points + 1). + ''' + return sum(map(GET_REAL_COMPLEXITY, self.classes)) - len(self.classes) + + @property + def total_complexity(self): + '''The total complexity. Computed adding up the visitor complexity, the + functions complexity, and the classes complexity. + ''' + return (self.complexity + self.functions_complexity + + self.classes_complexity + (not self.off)) + + @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 + # The Try/Except block is counted as the number of handlers + # plus the `else` block. + # In Python 3.3 the TryExcept and TryFinally nodes have been merged + # into a single node: Try + if name in ('Try', 'TryExcept'): + self.complexity += len(node.handlers) + len(node.orelse) + elif name == 'BoolOp': + self.complexity += len(node.values) - 1 + # Ifs, with and assert statements count all as 1. + # Note: Lambda functions are not counted anymore, see #68 + elif name in ('If', 'IfExp'): + self.complexity += 1 + # The For and While blocks count as 1 plus the `else` block. + elif name in ('For', 'While', 'AsyncFor'): + self.complexity += bool(node.orelse) + 1 + # List, set, dict comprehensions and generator exps count as 1 plus + # the `if` statement. + elif name == 'comprehension': + self.complexity += len(node.ifs) + 1 + + super(RawVisitor, self).generic_visit(node) + + def visit_Assert(self, node): + '''When visiting `assert` statements, the complexity is increased only + if the `no_assert` attribute is `False`. + ''' + self.complexity += not self.no_assert + + def visit_AsyncFunctionDef(self, node): + '''Async function definition is the same thing as the synchronous + one. + ''' + self.visit_FunctionDef(node) + + def visit_FunctionDef(self, node): + '''When visiting functions a new visitor is created to recursively + analyze the function's body. + ''' + # The complexity of a function is computed taking into account + # the following factors: number of decorators, the complexity + # the function's body and the number of closures (which count + # double). + closures = [] + body_complexity = 1 + for child in node.body: + visitor = RawVisitor(off=False, no_assert=self.no_assert) + visitor.visit(child) + closures.extend(visitor.functions) + # Add general complexity but not closures' complexity, see #68 + body_complexity += visitor.complexity + + code = unparse(child) + raw_metrics = analyze(code) + raw_metrics_dict = raw_to_dict(raw_metrics) + + func = RawFunction(node.name, node.lineno, node.col_offset, + max(node.lineno, visitor.max_line), self.to_method, + self.classname, closures, body_complexity, + raw_metrics_dict['loc'], + raw_metrics_dict['lloc'], + raw_metrics_dict['sloc'], + raw_metrics_dict['comments'], + raw_metrics_dict['multi'], + raw_metrics_dict['blank'], + raw_metrics_dict['single_comments'], + ) + self.functions.append(func) + + def visit_ClassDef(self, node): + '''When visiting classes a new visitor is created to recursively + analyze the class' body and methods. + ''' + # The complexity of a class is computed taking into account + # the following factors: number of decorators and the complexity + # of the class' body (which is the sum of all the complexities). + methods = [] + # According to Cyclomatic Complexity definition it has to start off + # from 1. + body_complexity = 1 + classname = node.name + visitors_max_lines = [node.lineno] + inner_classes = [] + for child in node.body: + visitor = RawVisitor(True, classname, off=False, + no_assert=self.no_assert) + visitor.visit(child) + methods.extend(visitor.functions) + body_complexity += (visitor.complexity + + visitor.functions_complexity + + len(visitor.functions)) + visitors_max_lines.append(visitor.max_line) + inner_classes.extend(visitor.classes) + + code = unparse(child) + raw_metrics = analyze(code) + raw_metrics_dict = raw_to_dict(raw_metrics) + + cls = RawClass(classname, node.lineno, node.col_offset, + max(visitors_max_lines + list(map(GET_ENDLINE, methods))), + methods, inner_classes, body_complexity, + raw_metrics_dict['loc'], + raw_metrics_dict['lloc'], + raw_metrics_dict['sloc'], + raw_metrics_dict['comments'], + raw_metrics_dict['multi'], + raw_metrics_dict['blank'], + raw_metrics_dict['single_comments'], + ) + self.classes.append(cls) + diff --git a/requirements.txt b/requirements.txt index 92da0cd..b7ff29f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ mando>=0.6,<0.7 colorama==0.4.1 flake8_polyfill future +astunparse diff --git a/setup.py b/setup.py index babd49c..c4ce706 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 'colorama==0.4.1', 'flake8-polyfill', 'future', + 'astunparse', ], entry_points={ 'console_scripts': ['radon = radon:main'], From eccce5fb8ad8c6a621561dcd6f7f359e36887e2c Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Sun, 28 Jun 2020 11:50:57 -0700 Subject: [PATCH 2/8] BUG: collecting raw matrics outside of for loop --- radon/raw_visitor.py | 68 +++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/radon/raw_visitor.py b/radon/raw_visitor.py index 6159d7f..b7463f8 100644 --- a/radon/raw_visitor.py +++ b/radon/raw_visitor.py @@ -53,7 +53,7 @@ ]) -class RawFunction(BaseRawFuncMetrics): +class RawFunctionMetrics(BaseRawFuncMetrics): '''Object represeting a function block.''' @property @@ -83,7 +83,7 @@ def __str__(self): self.complexity) -class RawClass(BaseRawClassMetrics): +class RawClassMetrics(BaseRawClassMetrics): '''Object representing a class block.''' letter = 'C' @@ -91,7 +91,7 @@ class RawClass(BaseRawClassMetrics): @property def fullname(self): '''The full name of the class. It is just its name. This attribute - exists for consistency (see :data:`RawFunction.fullname`). + exists for consistency (see :data:`RawFunctionMetrics.fullname`). ''' return self.name @@ -251,6 +251,18 @@ def visit_AsyncFunctionDef(self, node): ''' self.visit_FunctionDef(node) + def get_raw_metrics(self, node): + code = unparse(node) + raw_metrics = analyze(code) + 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. @@ -261,6 +273,7 @@ def visit_FunctionDef(self, node): # double). closures = [] body_complexity = 1 + for child in node.body: visitor = RawVisitor(off=False, no_assert=self.no_assert) visitor.visit(child) @@ -268,22 +281,20 @@ def visit_FunctionDef(self, node): # Add general complexity but not closures' complexity, see #68 body_complexity += visitor.complexity - code = unparse(child) - raw_metrics = analyze(code) - raw_metrics_dict = raw_to_dict(raw_metrics) - - func = RawFunction(node.name, node.lineno, node.col_offset, + 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, body_complexity, - raw_metrics_dict['loc'], - raw_metrics_dict['lloc'], - raw_metrics_dict['sloc'], - raw_metrics_dict['comments'], - raw_metrics_dict['multi'], - raw_metrics_dict['blank'], - raw_metrics_dict['single_comments'], + self.loc, + self.lloc, + self.sloc, + self.comments, + self.multi, + self.blank, + self.single_comments, ) - self.functions.append(func) + + self.functions.append(func_metrics) def visit_ClassDef(self, node): '''When visiting classes a new visitor is created to recursively @@ -309,21 +320,18 @@ def visit_ClassDef(self, node): len(visitor.functions)) visitors_max_lines.append(visitor.max_line) inner_classes.extend(visitor.classes) - - code = unparse(child) - raw_metrics = analyze(code) - raw_metrics_dict = raw_to_dict(raw_metrics) - - cls = RawClass(classname, node.lineno, node.col_offset, + + 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, body_complexity, - raw_metrics_dict['loc'], - raw_metrics_dict['lloc'], - raw_metrics_dict['sloc'], - raw_metrics_dict['comments'], - raw_metrics_dict['multi'], - raw_metrics_dict['blank'], - raw_metrics_dict['single_comments'], + self.loc, + self.lloc, + self.sloc, + self.comments, + self.multi, + self.blank, + self.single_comments, ) - self.classes.append(cls) + self.classes.append(cls_metrics) From 898307411ecaff7f96fcb45e0f303518a59d1dc8 Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Wed, 1 Jul 2020 18:02:56 -0700 Subject: [PATCH 3/8] feat(raw_visitor): removing complexity_visitor code and revining raw_visitor --- radon/raw_visitor.py | 133 +++++++++++-------------------------------- 1 file changed, 34 insertions(+), 99 deletions(-) diff --git a/radon/raw_visitor.py b/radon/raw_visitor.py index b7463f8..eb4bd74 100644 --- a/radon/raw_visitor.py +++ b/radon/raw_visitor.py @@ -1,7 +1,7 @@ from collections import namedtuple import ast - -from astunparse import unparse +import inspect +import sys from radon.metrics import analyze from radon.visitors import ( @@ -12,6 +12,12 @@ ) 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', @@ -23,7 +29,6 @@ 'is_method', 'classname', 'closures', - 'complexity', 'loc', 'lloc', 'sloc', @@ -42,7 +47,6 @@ 'endline', 'methods', 'inner_classes', - 'real_complexity', 'loc', 'lloc', 'sloc', @@ -76,11 +80,11 @@ def fullname(self): def __str__(self): '''String representation of a function block.''' - return '{0} {1}:{2}->{3} {4} - {5}'.format(self.letter, self.lineno, + return '{0} {1}:{2}->{3} {4} - sloc: {5}'.format(self.letter, self.lineno, self.col_offset, self.endline, self.fullname, - self.complexity) + self.sloc) class RawClassMetrics(BaseRawClassMetrics): @@ -95,22 +99,12 @@ def fullname(self): ''' return self.name - @property - def complexity(self): - '''The average complexity of the class. It corresponds to the average - complexity of its methods plus one. - ''' - if not self.methods: - return self.real_complexity - methods = len(self.methods) - return int(self.real_complexity / float(methods)) + (methods > 1) - def __str__(self): '''String representation of a class block.''' - return '{0} {1}:{2}->{3} {4} - {5}'.format(self.letter, self.lineno, + return '{0} {1}:{2}->{3} {4} - sloc: {5}'.format(self.letter, self.lineno, self.col_offset, self.endline, self.name, - self.complexity) + self.sloc) class CodeVisitor(ast.NodeVisitor): @@ -125,10 +119,12 @@ def get_name(obj): @classmethod def from_code(cls, code, **kwargs): - '''Instanciate the class from source code (string object). The + '''Instantiate the class from source code (string object). The `**kwargs` are directly passed to the `ast.NodeVisitor` constructor. ''' - return cls.from_ast(code2ast(code), **kwargs) + cls.code = code + node = code2ast(code) + return cls.from_ast(node, **kwargs) @classmethod def from_ast(cls, ast_node, **kwargs): @@ -141,8 +137,9 @@ def from_ast(cls, ast_node, **kwargs): class RawVisitor(CodeVisitor): - '''A visitor that keeps track of the cyclomatic complexity of - the elements. + '''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. @@ -151,41 +148,13 @@ class RawVisitor(CodeVisitor): otherwise to 0. ''' - def __init__(self, to_method=False, classname=None, off=True, - no_assert=False): - self.off = off - self.complexity = 1 if off else 0 + def __init__(self, to_method=False, classname=None): self.functions = [] self.classes = [] self.to_method = to_method self.classname = classname - self.no_assert = no_assert self._max_line = float('-inf') - @property - def functions_complexity(self): - '''The total complexity from all functions (i.e. the total number of - decision points + 1). - - This is *not* the sum of all the complexity from the functions. Rather, - it's the complexity of the code *inside* all the functions. - ''' - return sum(map(GET_COMPLEXITY, self.functions)) - len(self.functions) - - @property - def classes_complexity(self): - '''The total complexity from all classes (i.e. the total number of - decision points + 1). - ''' - return sum(map(GET_REAL_COMPLEXITY, self.classes)) - len(self.classes) - - @property - def total_complexity(self): - '''The total complexity. Computed adding up the visitor complexity, the - functions complexity, and the classes complexity. - ''' - return (self.complexity + self.functions_complexity + - self.classes_complexity + (not self.off)) @property def blocks(self): @@ -217,34 +186,9 @@ def generic_visit(self, node): # Check for a lineno attribute if hasattr(node, 'lineno'): self.max_line = node.lineno - # The Try/Except block is counted as the number of handlers - # plus the `else` block. - # In Python 3.3 the TryExcept and TryFinally nodes have been merged - # into a single node: Try - if name in ('Try', 'TryExcept'): - self.complexity += len(node.handlers) + len(node.orelse) - elif name == 'BoolOp': - self.complexity += len(node.values) - 1 - # Ifs, with and assert statements count all as 1. - # Note: Lambda functions are not counted anymore, see #68 - elif name in ('If', 'IfExp'): - self.complexity += 1 - # The For and While blocks count as 1 plus the `else` block. - elif name in ('For', 'While', 'AsyncFor'): - self.complexity += bool(node.orelse) + 1 - # List, set, dict comprehensions and generator exps count as 1 plus - # the `if` statement. - elif name == 'comprehension': - self.complexity += len(node.ifs) + 1 super(RawVisitor, self).generic_visit(node) - def visit_Assert(self, node): - '''When visiting `assert` statements, the complexity is increased only - if the `no_assert` attribute is `False`. - ''' - self.complexity += not self.no_assert - def visit_AsyncFunctionDef(self, node): '''Async function definition is the same thing as the synchronous one. @@ -252,8 +196,13 @@ def visit_AsyncFunctionDef(self, node): self.visit_FunctionDef(node) def get_raw_metrics(self, node): - code = unparse(node) - raw_metrics = analyze(code) + # 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'] @@ -267,24 +216,17 @@ def visit_FunctionDef(self, node): '''When visiting functions a new visitor is created to recursively analyze the function's body. ''' - # The complexity of a function is computed taking into account - # the following factors: number of decorators, the complexity - # the function's body and the number of closures (which count - # double). closures = [] - body_complexity = 1 for child in node.body: - visitor = RawVisitor(off=False, no_assert=self.no_assert) + visitor = RawVisitor() visitor.visit(child) closures.extend(visitor.functions) - # Add general complexity but not closures' complexity, see #68 - body_complexity += visitor.complexity 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, body_complexity, + self.classname, closures, self.loc, self.lloc, self.sloc, @@ -300,31 +242,24 @@ def visit_ClassDef(self, node): '''When visiting classes a new visitor is created to recursively analyze the class' body and methods. ''' - # The complexity of a class is computed taking into account - # the following factors: number of decorators and the complexity - # of the class' body (which is the sum of all the complexities). methods = [] - # According to Cyclomatic Complexity definition it has to start off - # from 1. - body_complexity = 1 classname = node.name visitors_max_lines = [node.lineno] inner_classes = [] for child in node.body: - visitor = RawVisitor(True, classname, off=False, - no_assert=self.no_assert) + visitor = RawVisitor( + True, + classname, + ) visitor.visit(child) methods.extend(visitor.functions) - body_complexity += (visitor.complexity + - visitor.functions_complexity + - len(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, body_complexity, + methods, inner_classes, self.loc, self.lloc, self.sloc, From b535d0b7814fbd7019ea3917fc7a90f7ebd6c642 Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Wed, 1 Jul 2020 18:05:08 -0700 Subject: [PATCH 4/8] BUG(tests_ipynb): "/"-> "\\" tests not passing with directory string --- radon/tests/test_ipynb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radon/tests/test_ipynb.py b/radon/tests/test_ipynb.py index ff80a04..435659a 100644 --- a/radon/tests/test_ipynb.py +++ b/radon/tests/test_ipynb.py @@ -28,7 +28,7 @@ @pytest.mark.skipif(not SUPPORTS_IPYNB, reason="nbformat not installed") def test_harvestor_yields_ipynb(log_mock): '''Test that Harvester will try ipynb files when configured''' - target = os.path.join(DIRNAME, 'data/example.ipynb') + target = os.path.join(DIRNAME, 'data\\example.ipynb') harvester = Harvester([DIRNAME], BASE_CONFIG_WITH_IPYNB) filenames = list(harvester._iter_filenames()) assert _is_python_file(target) @@ -110,7 +110,7 @@ def test_raw_ipynb(log_mock): **BASE_CONFIG_WITH_IPYNB.config_values ) - target = os.path.join(DIRNAME, 'data/example.ipynb') + target = os.path.join(DIRNAME, 'data\\example.ipynb') harvester = RawHarvester([DIRNAME], raw_cfg) out = json.loads(harvester.as_json()) assert harvester.config.include_ipynb == True @@ -130,7 +130,7 @@ def test_raw_ipynb_cells(log_mock): **BASE_CONFIG_WITH_IPYNB_AND_CELLS.config_values ) - target = os.path.join(DIRNAME, 'data/example.ipynb') + target = os.path.join(DIRNAME, 'data\\example.ipynb') harvester = RawHarvester([DIRNAME], raw_cfg) out = json.loads(harvester.as_json()) cell_target = target + ":[3]" From 99cdf03cb6b438be5c5cde70e5f4153fce9655ee Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Wed, 1 Jul 2020 18:06:32 -0700 Subject: [PATCH 5/8] test(test_raw_visitor): a first cut at testing raw_visitor --- radon/tests/test_raw_visitor.py | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 radon/tests/test_raw_visitor.py diff --git a/radon/tests/test_raw_visitor.py b/radon/tests/test_raw_visitor.py new file mode 100644 index 0000000..ff9f511 --- /dev/null +++ b/radon/tests/test_raw_visitor.py @@ -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 \ No newline at end of file From 322f4fa9eeaa5a2b0c40fd9f11c2b660e6cf1544 Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Sat, 19 Sep 2020 15:49:18 -0700 Subject: [PATCH 6/8] delete unused tests --- radon/tests/test_raw_visitor.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/radon/tests/test_raw_visitor.py b/radon/tests/test_raw_visitor.py index ff9f511..4f42645 100644 --- a/radon/tests/test_raw_visitor.py +++ b/radon/tests/test_raw_visitor.py @@ -6,7 +6,10 @@ # 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:] +reuseable_tests = [test_raw.ANALYZE_CASES[5]] \ + + 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) @@ -21,17 +24,3 @@ def test_raw_visitor_functions(code, expected): + 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 \ No newline at end of file From 35f7d28a902061dc6dfed3f19b7ac9428413d560 Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Sat, 19 Sep 2020 16:47:39 -0700 Subject: [PATCH 7/8] revert double back slash to forward slash --- radon/tests/test_ipynb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radon/tests/test_ipynb.py b/radon/tests/test_ipynb.py index 435659a..ff80a04 100644 --- a/radon/tests/test_ipynb.py +++ b/radon/tests/test_ipynb.py @@ -28,7 +28,7 @@ @pytest.mark.skipif(not SUPPORTS_IPYNB, reason="nbformat not installed") def test_harvestor_yields_ipynb(log_mock): '''Test that Harvester will try ipynb files when configured''' - target = os.path.join(DIRNAME, 'data\\example.ipynb') + target = os.path.join(DIRNAME, 'data/example.ipynb') harvester = Harvester([DIRNAME], BASE_CONFIG_WITH_IPYNB) filenames = list(harvester._iter_filenames()) assert _is_python_file(target) @@ -110,7 +110,7 @@ def test_raw_ipynb(log_mock): **BASE_CONFIG_WITH_IPYNB.config_values ) - target = os.path.join(DIRNAME, 'data\\example.ipynb') + target = os.path.join(DIRNAME, 'data/example.ipynb') harvester = RawHarvester([DIRNAME], raw_cfg) out = json.loads(harvester.as_json()) assert harvester.config.include_ipynb == True @@ -130,7 +130,7 @@ def test_raw_ipynb_cells(log_mock): **BASE_CONFIG_WITH_IPYNB_AND_CELLS.config_values ) - target = os.path.join(DIRNAME, 'data\\example.ipynb') + target = os.path.join(DIRNAME, 'data/example.ipynb') harvester = RawHarvester([DIRNAME], raw_cfg) out = json.loads(harvester.as_json()) cell_target = target + ":[3]" From 22a9bd2ba5acfd7a4c182fc0b8b1d3a25e6c8a3e Mon Sep 17 00:00:00 2001 From: Blane Grann Date: Sat, 19 Sep 2020 16:59:56 -0700 Subject: [PATCH 8/8] fix: linting, separate test cases for reusability --- radon/raw_visitor.py | 248 ++++++++++++++++---------------- radon/tests/test_raw.py | 128 +++++++++-------- radon/tests/test_raw_visitor.py | 46 ++++-- 3 files changed, 224 insertions(+), 198 deletions(-) diff --git a/radon/raw_visitor.py b/radon/raw_visitor.py index eb4bd74..1c37dd9 100644 --- a/radon/raw_visitor.py +++ b/radon/raw_visitor.py @@ -1,143 +1,144 @@ 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.visitors import GET_ENDLINE, 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') - + raise ImportError("raw_visitor module requires Python >=3.8") BaseRawFuncMetrics = namedtuple( - 'BaseRawFuncMetrics', + "BaseRawFuncMetrics", [ - 'name', - 'lineno', - 'col_offset', - 'endline', - 'is_method', - 'classname', - 'closures', - 'loc', - 'lloc', - 'sloc', - 'comments', - 'multi', - 'blank', - 'single_comments', - ]) + "name", + "lineno", + "col_offset", + "endline", + "is_method", + "classname", + "closures", + "loc", + "lloc", + "sloc", + "comments", + "multi", + "blank", + "single_comments", + ], +) BaseRawClassMetrics = namedtuple( - 'BaseRawClassMetrics', + "BaseRawClassMetrics", [ - 'name', - 'lineno', - 'col_offset', - 'endline', - 'methods', - 'inner_classes', - 'loc', - 'lloc', - 'sloc', - 'comments', - 'multi', - 'blank', - 'single_comments', - ]) + "name", + "lineno", + "col_offset", + "endline", + "methods", + "inner_classes", + "loc", + "lloc", + "sloc", + "comments", + "multi", + "blank", + "single_comments", + ], +) class RawFunctionMetrics(BaseRawFuncMetrics): - '''Object represeting a function block.''' + """Object represeting a function block.""" @property def letter(self): - '''The letter representing the function. It is `M` if the function is + """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' + """ + 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 + """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) + 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) + """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.''' + """Object representing a class block.""" - letter = 'C' + letter = "C" @property def fullname(self): - '''The full name of the class. It is just its name. This attribute + """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) + """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 + """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__``.''' + """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 + """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 + """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. + """A visitor that keeps track of raw metrics for block of code. Metrics are provided for functions, classes and class methods. @@ -146,21 +147,20 @@ class RawVisitor(CodeVisitor): :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') - + self._max_line = float("-inf") @property def blocks(self): - '''All the blocks visited. These include: all the functions, the + """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: @@ -170,29 +170,27 @@ def blocks(self): @property def max_line(self): - '''The maximum line number among the analyzed lines.''' + """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.''' + """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) + """Main entry point for the visitor.""" # Check for a lineno attribute - if hasattr(node, 'lineno'): + 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 + """Async function definition is the same thing as the synchronous one. - ''' + """ self.visit_FunctionDef(node) def get_raw_metrics(self, node): @@ -204,44 +202,49 @@ def get_raw_metrics(self, 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'] + 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 + """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, - ) + 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 + """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] @@ -250,23 +253,26 @@ def visit_ClassDef(self, node): 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, - ) + 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) - diff --git a/radon/tests/test_raw.py b/radon/tests/test_raw.py index b9bd0f2..ec73a7b 100644 --- a/radon/tests/test_raw.py +++ b/radon/tests/test_raw.py @@ -136,40 +136,7 @@ def test_logical(code, expected_number_of_lines): assert _logical(code) == expected_number_of_lines -ANALYZE_CASES = [ - (''' - ''', (0, 0, 0, 0, 0, 0, 0)), - - (''' - """ - doc? - """ - ''', (3, 1, 0, 0, 3, 0, 0)), - - (''' - # just a comment - if a and b: - print('woah') - else: - # you'll never get here - print('ven') - ''', (6, 4, 4, 2, 0, 0, 2)), - - (''' - # - # - # - ''', (3, 0, 0, 3, 0, 0, 3)), - - (''' - if a: - print - - - else: - print - ''', (6, 4, 4, 0, 0, 2, 0)), - +VISITOR_CASES = [ # In this case the docstring is not counted as a multi-line string # because in fact it is on one line! (''' @@ -178,34 +145,6 @@ def f(n): return n * f(n - 1) ''', (3, 3, 2, 0, 0, 0, 1)), - (''' - def hip(a, k): - if k == 1: return a - # getting high... - return a ** hip(a, k - 1) - - def fib(n): - """Compute the n-th Fibonacci number. - - Try it with n = 294942: it will take a fairly long time. - """ - if n <= 1: return 1 # otherwise it will melt the cpu - return fib(n - 2) + fib(n - 1) - ''', (12, 9, 6, 2, 3, 2, 1)), - - (''' - a = [1, 2, 3, - ''', SyntaxError), - - # Test that handling of parameters with a value passed in. - (''' - def foo(n=1): - """ - Try it with n = 294942: it will take a fairly long time. - """ - if n <= 1: return 1 # otherwise it will melt the cpu - ''', (5, 4, 2, 1, 3, 0, 0)), - (''' def foo(n=1): """ @@ -332,8 +271,71 @@ def function(): ''', (2, 3, 2, 0, 0, 0, 0)), ] +MAIN_CASES = [ + (''' + ''', (0, 0, 0, 0, 0, 0, 0)), + + (''' + """ + doc? + """ + ''', (3, 1, 0, 0, 3, 0, 0)), + + (''' + # just a comment + if a and b: + print('woah') + else: + # you'll never get here + print('ven') + ''', (6, 4, 4, 2, 0, 0, 2)), + + (''' + # + # + # + ''', (3, 0, 0, 3, 0, 0, 3)), + + (''' + if a: + print + + + else: + print + ''', (6, 4, 4, 0, 0, 2, 0)), + + (''' + def hip(a, k): + if k == 1: return a + # getting high... + return a ** hip(a, k - 1) + + def fib(n): + """Compute the n-th Fibonacci number. + + Try it with n = 294942: it will take a fairly long time. + """ + if n <= 1: return 1 # otherwise it will melt the cpu + return fib(n - 2) + fib(n - 1) + ''', (12, 9, 6, 2, 3, 2, 1)), + + (''' + a = [1, 2, 3, + ''', SyntaxError), + + # Test that handling of parameters with a value passed in. + (''' + def foo(n=1): + """ + Try it with n = 294942: it will take a fairly long time. + """ + if n <= 1: return 1 # otherwise it will melt the cpu + ''', (5, 4, 2, 1, 3, 0, 0)), +] + -@pytest.mark.parametrize('code,expected', ANALYZE_CASES) +@pytest.mark.parametrize('code,expected', MAIN_CASES + VISITOR_CASES) def test_analyze(code, expected): code = dedent(code) diff --git a/radon/tests/test_raw_visitor.py b/radon/tests/test_raw_visitor.py index 4f42645..307004c 100644 --- a/radon/tests/test_raw_visitor.py +++ b/radon/tests/test_raw_visitor.py @@ -1,16 +1,26 @@ +import sys + import pytest -from radon.raw import Module, analyze -from radon.raw_visitor import RawVisitor +from radon.raw import Module 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[5]] \ - + test_raw.ANALYZE_CASES[9:13] \ - + test_raw.ANALYZE_CASES[14:] +# we expect an ImportError when python <3.8 +try: + from radon.raw_visitor import RawVisitor + + IMPORT_ERROR = False +except ImportError: + IMPORT_ERROR = True + +min_py_version = pytest.mark.xfail( + IMPORT_ERROR and sys.version_info < (3, 8), + reason="raw_visitor requires python >=3.8", +) -@pytest.mark.parametrize('code, expected', reuseable_tests) + +@min_py_version +@pytest.mark.parametrize("code, expected", test_raw.VISITOR_CASES) def test_raw_visitor_functions(code, expected): code = test_raw.dedent(code) raw_visitor = RawVisitor.from_code(code) @@ -18,9 +28,17 @@ def test_raw_visitor_functions(code, expected): 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 + assert formated_result == Module( + *expected + ), f"\ + \n input code: {code}\ + \n result: {formated_result} \ + \n expected: {Module(*expected)}" + + expected_loc = ( + formated_result.blank + + formated_result.sloc + + formated_result.single_comments + + formated_result.multi + ) + assert formated_result.loc == expected_loc