RadonMetrics/radon/complexity.py

changeset 1
b6cced815847
child 55
755bc8e1485a
equal deleted inserted replaced
0:765bb3e711d6 1:b6cced815847
1 '''This module contains all high-level helpers function that allow to work with
2 Cyclomatic Complexity
3 '''
4
5 import math
6 from radon.visitors import GET_COMPLEXITY, ComplexityVisitor, code2ast
7
8
9 # sorted_block ordering functions
10 SCORE = lambda block: -GET_COMPLEXITY(block)
11 LINES = lambda block: block.lineno
12 ALPHA = lambda block: block.name
13
14
15 def cc_rank(cc):
16 r'''Rank the complexity score from A to F, where A stands for the simplest
17 and best score and F the most complex and worst one:
18
19 ============= =====================================================
20 1 - 5 A (low risk - simple block)
21 6 - 10 B (low risk - well structured and stable block)
22 11 - 20 C (moderate risk - slightly complex block)
23 21 - 30 D (more than moderate risk - more complex block)
24 31 - 40 E (high risk - complex block, alarming)
25 41+ F (very high risk - error-prone, unstable block)
26 ============= =====================================================
27
28 Here *block* is used in place of function, method or class.
29
30 The formula used to convert the score into an index is the following:
31
32 .. math::
33
34 \text{rank} = \left \lceil \dfrac{\text{score}}{10} \right \rceil
35 - H(5 - \text{score})
36
37 where ``H(s)`` stands for the Heaviside Step Function.
38 The rank is then associated to a letter (0 = A, 5 = F).
39 '''
40 if cc < 0:
41 raise ValueError('Complexity must be a non-negative value')
42 return chr(min(int(math.ceil(cc / 10.) or 1) - (1, 0)[5 - cc < 0], 5) + 65)
43
44
45 def average_complexity(blocks):
46 '''Compute the average Cyclomatic complexity from the given blocks.
47 Blocks must be either :class:`~radon.visitors.Function` or
48 :class:`~radon.visitors.Class`. If the block list is empty, then 0 is
49 returned.
50 '''
51 size = len(blocks)
52 if size == 0:
53 return 0
54 return sum((GET_COMPLEXITY(block) for block in blocks), .0) / len(blocks)
55
56
57 def sorted_results(blocks, order=SCORE):
58 '''Given a ComplexityVisitor instance, returns a list of sorted blocks
59 with respect to complexity. A block is a either
60 :class:`~radon.visitors.Function` object or a
61 :class:`~radon.visitors.Class` object.
62 The blocks are sorted in descending order from the block with the highest
63 complexity.
64
65 The optional `order` parameter indicates how to sort the blocks. It can be:
66
67 * `LINES`: sort by line numbering;
68 * `ALPHA`: sort by name (from A to Z);
69 * `SCORE`: sorty by score (descending).
70
71 Default is `SCORE`.
72 '''
73 return sorted(blocks, key=order)
74
75
76 def add_closures(blocks):
77 '''Process a list of blocks by adding all closures as top-level blocks.'''
78 new_blocks = []
79 for block in blocks:
80 new_blocks.append(block)
81 if 'closures' not in block._fields:
82 continue
83 for closure in block.closures:
84 named = closure._replace(name=block.name + '.' + closure.name)
85 new_blocks.append(named)
86 return new_blocks
87
88
89 def cc_visit(code, **kwargs):
90 '''Visit the given code with :class:`~radon.visitors.ComplexityVisitor`.
91 All the keyword arguments are directly passed to the visitor.
92 '''
93 return cc_visit_ast(code2ast(code), **kwargs)
94
95
96 def cc_visit_ast(ast_node, **kwargs):
97 '''Visit the AST node with :class:`~radon.visitors.ComplexityVisitor`. All
98 the keyword arguments are directly passed to the visitor.
99 '''
100 return ComplexityVisitor.from_ast(ast_node, **kwargs).blocks
101
102
103 class Flake8Checker(object):
104 '''Entry point for the Flake8 tool.'''
105
106 name = 'radon'
107 _code = 'R701'
108 _error_tmpl = 'R701: %r is too complex (%d)'
109 no_assert = False
110 max_cc = -1
111
112 def __init__(self, tree, filename):
113 '''Accept the AST tree and a filename (unused).'''
114 self.tree = tree
115
116 version = property(lambda self: __import__('radon').__version__)
117
118 @classmethod
119 def add_options(cls, parser): # pragma: no cover
120 '''Add custom options to the global parser.'''
121 parser.add_option('--radon-max-cc', default=-1, action='store',
122 type='int', help='Radon complexity threshold')
123 parser.add_option('--radon-no-assert', dest='no_assert',
124 action='store_true', default=False,
125 help='Radon will ignore assert statements')
126 parser.config_options.append('radon-max-cc')
127 parser.config_options.append('radon-no-assert')
128
129 @classmethod
130 def parse_options(cls, options): # pragma: no cover
131 '''Save actual options as class attributes.'''
132 cls.max_cc = options.radon_max_cc
133 cls.no_assert = options.no_assert
134
135 def run(self):
136 '''Run the ComplexityVisitor over the AST tree.'''
137 if self.max_cc < 0:
138 if not self.no_assert:
139 return
140 self.max_cc = 10
141 visitor = ComplexityVisitor.from_ast(self.tree,
142 no_assert=self.no_assert)
143 for block in visitor.blocks:
144 if block.complexity > self.max_cc:
145 text = self._error_tmpl % (block.name, block.complexity)
146 yield block.lineno, 0, text, type(self)

eric ide

mercurial