Added the radon files.

Sun, 13 Sep 2015 17:56:31 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 13 Sep 2015 17:56:31 +0200
changeset 1
b6cced815847
parent 0
765bb3e711d6
child 2
1ad320a50a01

Added the radon files.

RadonMetrics/radon/__init__.py file | annotate | diff | comparison | revisions
RadonMetrics/radon/complexity.py file | annotate | diff | comparison | revisions
RadonMetrics/radon/metrics.py file | annotate | diff | comparison | revisions
RadonMetrics/radon/raw.py file | annotate | diff | comparison | revisions
RadonMetrics/radon/visitors.py file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadonMetrics/radon/__init__.py	Sun Sep 13 17:56:31 2015 +0200
@@ -0,0 +1,5 @@
+"""
+This module contains the version info.
+"""
+
+__version__ = '1.2.2'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadonMetrics/radon/complexity.py	Sun Sep 13 17:56:31 2015 +0200
@@ -0,0 +1,146 @@
+'''This module contains all high-level helpers function that allow to work with
+Cyclomatic Complexity
+'''
+
+import math
+from radon.visitors import GET_COMPLEXITY, ComplexityVisitor, code2ast
+
+
+# sorted_block ordering functions
+SCORE = lambda block: -GET_COMPLEXITY(block)
+LINES = lambda block: block.lineno
+ALPHA = lambda block: block.name
+
+
+def cc_rank(cc):
+    r'''Rank the complexity score from A to F, where A stands for the simplest
+    and best score and F the most complex and worst one:
+
+    ============= =====================================================
+        1 - 5        A (low risk - simple block)
+        6 - 10       B (low risk - well structured and stable block)
+        11 - 20      C (moderate risk - slightly complex block)
+        21 - 30      D (more than moderate risk - more complex block)
+        31 - 40      E (high risk - complex block, alarming)
+        41+          F (very high risk - error-prone, unstable block)
+    ============= =====================================================
+
+    Here *block* is used in place of function, method or class.
+
+    The formula used to convert the score into an index is the following:
+
+    .. math::
+
+        \text{rank} = \left \lceil \dfrac{\text{score}}{10} \right \rceil
+        - H(5 - \text{score})
+
+    where ``H(s)`` stands for the Heaviside Step Function.
+    The rank is then associated to a letter (0 = A, 5 = F).
+    '''
+    if cc < 0:
+        raise ValueError('Complexity must be a non-negative value')
+    return chr(min(int(math.ceil(cc / 10.) or 1) - (1, 0)[5 - cc < 0], 5) + 65)
+
+
+def average_complexity(blocks):
+    '''Compute the average Cyclomatic complexity from the given blocks.
+    Blocks must be either :class:`~radon.visitors.Function` or
+    :class:`~radon.visitors.Class`. If the block list is empty, then 0 is
+    returned.
+    '''
+    size = len(blocks)
+    if size == 0:
+        return 0
+    return sum((GET_COMPLEXITY(block) for block in blocks), .0) / len(blocks)
+
+
+def sorted_results(blocks, order=SCORE):
+    '''Given a ComplexityVisitor instance, returns a list of sorted blocks
+    with respect to complexity. A block is a either
+    :class:`~radon.visitors.Function` object or a
+    :class:`~radon.visitors.Class` object.
+    The blocks are sorted in descending order from the block with the highest
+    complexity.
+
+    The optional `order` parameter indicates how to sort the blocks. It can be:
+
+        * `LINES`: sort by line numbering;
+        * `ALPHA`: sort by name (from A to Z);
+        * `SCORE`: sorty by score (descending).
+
+    Default is `SCORE`.
+    '''
+    return sorted(blocks, key=order)
+
+
+def add_closures(blocks):
+    '''Process a list of blocks by adding all closures as top-level blocks.'''
+    new_blocks = []
+    for block in blocks:
+        new_blocks.append(block)
+        if 'closures' not in block._fields:
+            continue
+        for closure in block.closures:
+            named = closure._replace(name=block.name + '.' + closure.name)
+            new_blocks.append(named)
+    return new_blocks
+
+
+def cc_visit(code, **kwargs):
+    '''Visit the given code with :class:`~radon.visitors.ComplexityVisitor`.
+    All the keyword arguments are directly passed to the visitor.
+    '''
+    return cc_visit_ast(code2ast(code), **kwargs)
+
+
+def cc_visit_ast(ast_node, **kwargs):
+    '''Visit the AST node with :class:`~radon.visitors.ComplexityVisitor`. All
+    the keyword arguments are directly passed to the visitor.
+    '''
+    return ComplexityVisitor.from_ast(ast_node, **kwargs).blocks
+
+
+class Flake8Checker(object):
+    '''Entry point for the Flake8 tool.'''
+
+    name = 'radon'
+    _code = 'R701'
+    _error_tmpl = 'R701: %r is too complex (%d)'
+    no_assert = False
+    max_cc = -1
+
+    def __init__(self, tree, filename):
+        '''Accept the AST tree and a filename (unused).'''
+        self.tree = tree
+
+    version = property(lambda self: __import__('radon').__version__)
+
+    @classmethod
+    def add_options(cls, parser):  # pragma: no cover
+        '''Add custom options to the global parser.'''
+        parser.add_option('--radon-max-cc', default=-1, action='store',
+                          type='int', help='Radon complexity threshold')
+        parser.add_option('--radon-no-assert', dest='no_assert',
+                          action='store_true', default=False,
+                          help='Radon will ignore assert statements')
+        parser.config_options.append('radon-max-cc')
+        parser.config_options.append('radon-no-assert')
+
+    @classmethod
+    def parse_options(cls, options):  # pragma: no cover
+        '''Save actual options as class attributes.'''
+        cls.max_cc = options.radon_max_cc
+        cls.no_assert = options.no_assert
+
+    def run(self):
+        '''Run the ComplexityVisitor over the AST tree.'''
+        if self.max_cc < 0:
+            if not self.no_assert:
+                return
+            self.max_cc = 10
+        visitor = ComplexityVisitor.from_ast(self.tree,
+                                             no_assert=self.no_assert)
+        for block in visitor.blocks:
+            if block.complexity > self.max_cc:
+                text = self._error_tmpl % (block.name, block.complexity)
+                yield block.lineno, 0, text, type(self)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadonMetrics/radon/metrics.py	Sun Sep 13 17:56:31 2015 +0200
@@ -0,0 +1,111 @@
+'''Module holding functions related to miscellaneous metrics, such as Halstead
+metrics or the Maintainability Index.
+'''
+
+import ast
+import math
+import collections
+from radon.visitors import HalsteadVisitor, ComplexityVisitor
+from radon.raw import analyze
+
+
+# Halstead metrics
+Halstead = collections.namedtuple('Halstead', 'h1 h2 N1 N2 vocabulary length '
+                                              'calculated_length volume '
+                                              'difficulty effort time bugs')
+
+
+def h_visit(code):
+    '''Compile the code into an AST tree and then pass it to
+    :func:`~radon.metrics.h_visit_ast`.
+    '''
+    return h_visit_ast(ast.parse(code))
+
+
+def h_visit_ast(ast_node):
+    '''Visit the AST node using the :class:`~radon.visitors.HalsteadVisitor`
+    visitor. A namedtuple with the following fields is returned:
+
+        * h1: the number of distinct operators
+        * h2: the number of distinct operands
+        * N1: the total number of operators
+        * N2: the total number of operands
+        * h: the vocabulary, i.e. h1 + h2
+        * N: the length, i.e. N1 + N2
+        * calculated_length: h1 * log2(h1) + h2 * log2(h2)
+        * volume: V = N * log2(h)
+        * difficulty: D = h1 / 2 * N2 / h2
+        * effort: E = D * V
+        * time: T = E / 18 seconds
+        * bugs: B = V / 3000 - an estimate of the errors in the implementation
+    '''
+    visitor = HalsteadVisitor.from_ast(ast_node)
+    h1, h2 = visitor.distinct_operators, visitor.distinct_operands
+    N1, N2 = visitor.operators, visitor.operands
+    h = h1 + h2
+    N = N1 + N2
+    if h1 and h2:
+        length = h1 * math.log(h1, 2) + h2 * math.log(h2, 2)
+    else:
+        length = 0
+    volume = N * math.log(h, 2) if h != 0 else 0
+    difficulty = (h1 * N2) / float(2 * h2) if h2 != 0 else 0
+    effort = difficulty * volume
+    return Halstead(
+        h1, h2, N1, N2, h, N, length, volume, difficulty, effort,
+        effort / 18., volume / 3000.
+    )
+
+
+def mi_compute(halstead_volume, complexity, sloc, comments):
+    '''Compute the Maintainability Index (MI) given the Halstead Volume, the
+    Cyclomatic Complexity, the SLOC number and the number of comment lines.
+    Usually it is not used directly but instead :func:`~radon.metrics.mi_visit`
+    is preferred.
+    '''
+    if any(metric <= 0 for metric in (halstead_volume, sloc)):
+        return 100.
+    sloc_scale = math.log(sloc)
+    volume_scale = math.log(halstead_volume)
+    comments_scale = math.sqrt(2.46 * math.radians(comments))
+    # Non-normalized MI
+    nn_mi = (171 - 5.2 * volume_scale - .23 * complexity - 16.2 * sloc_scale +
+             50 * math.sin(comments_scale))
+    return min(max(0., nn_mi * 100 / 171.), 100.)
+
+
+def mi_parameters(code, count_multi=True):
+    '''Given a source code snippet, compute the necessary parameters to
+    compute the Maintainability Index metric. These include:
+
+        * the Halstead Volume
+        * the Cyclomatic Complexity
+        * the number of LLOC (Logical Lines of Code)
+        * the percent of lines of comment
+
+    :param multi: If True, then count multiline strings as comment lines as
+        well. This is not always safe because Python multiline strings are not
+        always docstrings.
+    '''
+    ast_node = ast.parse(code)
+    raw = analyze(code)
+    comments_lines = raw.comments + (raw.multi if count_multi else 0)
+    comments = comments_lines / float(raw.sloc) * 100 if raw.sloc != 0 else 0
+    return (h_visit_ast(ast_node).volume,
+            ComplexityVisitor.from_ast(ast_node).total_complexity, raw.lloc,
+            comments)
+
+
+def mi_visit(code, multi):
+    '''Visit the code and compute the Maintainability Index (MI) from it.'''
+    return mi_compute(*mi_parameters(code, multi))
+
+
+def mi_rank(score):
+    r'''Rank the score with a letter:
+
+        * A if :math:`\text{score} > 19`;
+        * B if :math:`9 < \text{score} \le 19`;
+        * C if :math:`\text{score} \le 9`.
+    '''
+    return chr(65 + (9 - score >= 0) + (19 - score >= 0))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadonMetrics/radon/raw.py	Sun Sep 13 17:56:31 2015 +0200
@@ -0,0 +1,194 @@
+'''This module contains functions related to raw metrics.
+
+The main function is :func:`~radon.raw.analyze`, and should be the only one
+that is used.
+'''
+
+import tokenize
+import operator
+import collections
+try:
+    import StringIO as io
+except ImportError:  # pragma: no cover
+    import io
+
+
+__all__ = ['OP', 'COMMENT', 'TOKEN_NUMBER', 'NL', 'EM', 'Module', '_generate',
+           '_less_tokens', '_find', '_logical', 'analyze']
+
+COMMENT = tokenize.COMMENT
+OP = tokenize.OP
+NL = tokenize.NL
+EM = tokenize.ENDMARKER
+
+# Helper for map()
+TOKEN_NUMBER = operator.itemgetter(0)
+
+# A module object. It contains the following data:
+#   loc = Lines of Code (total lines)
+#   lloc = Logical Lines of Code
+#   comments = Comments lines
+#   blank = Blank lines (or whitespace-only lines)
+Module = collections.namedtuple('Module', ['loc', 'lloc', 'sloc',
+                                           'comments', 'multi', 'blank'])
+
+
+def _generate(code):
+    '''Pass the code into `tokenize.generate_tokens` and convert the result
+    into a list.
+    '''
+    return list(tokenize.generate_tokens(io.StringIO(code).readline))
+
+
+def _less_tokens(tokens, remove):
+    '''Process the output of `tokenize.generate_tokens` removing
+    the tokens specified in `remove`.
+    '''
+    for values in tokens:
+        if values[0] in remove:
+            continue
+        yield values
+
+
+def _find(tokens, token, value):
+    '''Return the position of the last token with the same (token, value)
+    pair supplied. The position is the one of the rightmost term.
+    '''
+    for index, token_values in enumerate(reversed(tokens)):
+        if (token, value) == token_values[:2]:
+            return len(tokens) - index - 1
+    raise ValueError('(token, value) pair not found')
+
+
+def _split_tokens(tokens, token, value):
+    '''Split a list of tokens on the specified token pair (token, value),
+    where *token* is the token type (i.e. its code) and *value* its actual
+    value in the code.
+    '''
+    res = [[]]
+    for token_values in tokens:
+        if (token, value) == token_values[:2]:
+            res.append([])
+            continue
+        res[-1].append(token_values)
+    return res
+
+
+def _get_all_tokens(line, lines):
+    '''Starting from *line*, generate the necessary tokens which represent the
+    shortest tokenization possible. This is done by catching
+    :exc:`tokenize.TokenError` when a multi-line string or statement is
+    encountered.
+    '''
+    sloc_increment = multi_increment = 0
+    try:
+        tokens = _generate(line)
+    except tokenize.TokenError:
+        # A multi-line string or statement has been encountered:
+        # start adding lines and stop when tokenize stops complaining
+        while True:
+            sloc_increment += 1
+            line = '\n'.join([line, next(lines)])
+            try:
+                tokens = _generate(line)
+            except tokenize.TokenError:
+                continue
+            if tokens[0][0] == 3 and len(tokens) == 2:
+                # Multi-line string detected
+                multi_increment += line.count('\n') + 1
+            break
+    return tokens, sloc_increment, multi_increment
+
+
+def _logical(tokens):
+    '''Find how many logical lines are there in the current line.
+
+    Normally 1 line of code is equivalent to 1 logical line of code,
+    but there are cases when this is not true. For example::
+
+        if cond: return 0
+
+    this line actually corresponds to 2 logical lines, since it can be
+    translated into::
+
+        if cond:
+            return 0
+
+    Examples::
+
+        if cond:  -> 1
+
+        if cond: return 0  -> 2
+
+        try: 1/0  -> 2
+
+        try:  -> 1
+
+        if cond:  # Only a comment  -> 1
+
+        if cond: return 0  # Only a comment  -> 2
+    '''
+    def aux(sub_tokens):
+        '''The actual function which does the job.'''
+        # Get the tokens and, in the meantime, remove comments
+        processed = list(_less_tokens(sub_tokens, [COMMENT]))
+        try:
+            # Verify whether a colon is present among the tokens and that
+            # it is the last token.
+            token_pos = _find(processed, OP, ':')
+            return 2 - (token_pos == len(processed) - 2)
+        except ValueError:
+            # The colon is not present
+            # If the line is only composed by comments, newlines and endmarker
+            # then it does not count as a logical line.
+            # Otherwise it count as 1.
+            if not list(_less_tokens(processed, [NL, EM])):
+                return 0
+            return 1
+    return sum(aux(sub) for sub in _split_tokens(tokens, OP, ';'))
+
+
+def analyze(source):
+    '''Analyze the source code and return a namedtuple with the following
+    fields:
+
+        * **loc**: The number of lines of code (total)
+        * **lloc**: The number of logical lines of code
+        * **sloc**: The number of source lines of code (not necessarily
+            corresponding to the LLOC)
+        * **comments**: The number of Python comment lines
+        * **multi**: The number of lines which represent multi-line strings
+        * **blank**: The number of blank lines (or whitespace-only ones)
+
+    The equation :math:`sloc + blanks = loc` should always hold.
+    Multiline strings are not counted as comments, since, to the Python
+    interpreter, they are not comments but strings.
+    '''
+    loc = sloc = lloc = comments = multi = blank = 0
+    lines = iter(source.splitlines())
+    for lineno, line in enumerate(lines, 1):
+        loc += 1
+        line = line.strip()
+        if not line:
+            blank += 1
+            continue
+        # If this is not a blank line, then it counts as a
+        # source line of code
+        sloc += 1
+        try:
+            # Process a logical line that spans on multiple lines
+            tokens, sloc_incr, multi_incr = _get_all_tokens(line, lines)
+        except StopIteration:
+            raise SyntaxError('SyntaxError at line: {0}'.format(lineno))
+        # Update tracked metrics
+        loc += sloc_incr  # LOC and SLOC increments are the same
+        sloc += sloc_incr
+        multi += multi_incr
+        # Add the comments
+        comments += list(map(TOKEN_NUMBER, tokens)).count(COMMENT)
+        # Process a logical line
+        # Split it on semicolons because they increase the number of logical
+        # lines
+        for sub_tokens in _split_tokens(tokens, OP, ';'):
+            lloc += _logical(sub_tokens)
+    return Module(loc, lloc, sloc, comments, multi, blank)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadonMetrics/radon/visitors.py	Sun Sep 13 17:56:31 2015 +0200
@@ -0,0 +1,366 @@
+'''This module contains the ComplexityVisitor class which is where all the
+analysis concerning Cyclomatic Complexity is done. There is also the class
+HalsteadVisitor, that counts Halstead metrics.'''
+
+import ast
+import operator
+import collections
+
+
+# Helper functions to use in combination with map()
+GET_COMPLEXITY = operator.attrgetter('complexity')
+GET_REAL_COMPLEXITY = operator.attrgetter('real_complexity')
+NAMES_GETTER = operator.attrgetter('name', 'asname')
+GET_ENDLINE = operator.attrgetter('endline')
+
+BaseFunc = collections.namedtuple('Function', ['name', 'lineno', 'col_offset',
+                                               'endline', 'is_method',
+                                               'classname', 'closures',
+                                               'complexity'])
+BaseClass = collections.namedtuple('Class', ['name', 'lineno', 'col_offset',
+                                             'endline', 'methods',
+                                             'real_complexity'])
+
+
+def code2ast(source):
+    '''Convert a string object into an AST object. This function attempts to
+    convert the string into bytes.
+    '''
+    try:
+        source = source.encode('utf-8')  # necessary in Python 3
+    except UnicodeDecodeError:  # pragma: no cover
+        pass
+    return ast.parse(source)
+
+
+class Function(BaseFunc):
+    '''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 Class(BaseClass):
+    '''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:`Function.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 ComplexityVisitor(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 ('With', 'If', 'IfExp'):
+            self.complexity += 1
+        # The For and While blocks count as 1 plus the `else` block.
+        elif name in ('For', 'While'):
+            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(ComplexityVisitor, 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_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 = ComplexityVisitor(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
+
+        func = Function(node.name, node.lineno, node.col_offset,
+                        max(node.lineno, visitor.max_line), self.to_method,
+                        self.classname, closures, body_complexity)
+        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]
+        for child in node.body:
+            visitor = ComplexityVisitor(True, classname, off=False,
+                                        no_assert=self.no_assert)
+            visitor.visit(child)
+            methods.extend(visitor.functions)
+            body_complexity += (visitor.complexity +
+                                visitor.functions_complexity)
+            visitors_max_lines.append(visitor.max_line)
+
+        cls = Class(classname, node.lineno, node.col_offset,
+                    max(visitors_max_lines + list(map(GET_ENDLINE, methods))),
+                    methods, body_complexity)
+        self.classes.append(cls)
+
+
+class HalsteadVisitor(CodeVisitor):
+    '''Visitor that keeps track of operators and operands, in order to compute
+    Halstead metrics (see :func:`radon.metrics.h_visit`).
+    '''
+
+    types = {ast.Num: 'n',
+             ast.Name: 'id',
+             ast.Attribute: 'attr'}
+
+    def __init__(self, context=None):
+        '''*context* is a string used to keep track the analysis' context.'''
+        self.operators_seen = set()
+        self.operands_seen = set()
+        self.operators = 0
+        self.operands = 0
+        self.context = context
+
+    @property
+    def distinct_operators(self):
+        '''The number of distinct operators.'''
+        return len(self.operators_seen)
+
+    @property
+    def distinct_operands(self):
+        '''The number of distinct operands.'''
+        return len(self.operands_seen)
+
+    def dispatch(meth):
+        '''This decorator does all the hard work needed for every node.
+
+        The decorated method must return a tuple of 4 elements:
+
+            * the number of operators
+            * the number of operands
+            * the operators seen (a sequence)
+            * the operands seen (a sequence)
+        '''
+        def aux(self, node):
+            '''Actual function that updates the stats.'''
+            results = meth(self, node)
+            self.operators += results[0]
+            self.operands += results[1]
+            self.operators_seen.update(results[2])
+            for operand in results[3]:
+                new_operand = getattr(operand,
+                                      self.types.get(type(operand), ''),
+                                      operand)
+
+                self.operands_seen.add((self.context, new_operand))
+            # Now dispatch to children
+            super(HalsteadVisitor, self).generic_visit(node)
+        return aux
+
+    @dispatch
+    def visit_BinOp(self, node):
+        '''A binary operator.'''
+        return (1, 2, (self.get_name(node.op),), (node.left, node.right))
+
+    @dispatch
+    def visit_UnaryOp(self, node):
+        '''A unary operator.'''
+        return (1, 1, (self.get_name(node.op),), (node.operand,))
+
+    @dispatch
+    def visit_BoolOp(self, node):
+        '''A boolean operator.'''
+        return (1, len(node.values), (self.get_name(node.op),), node.values)
+
+    @dispatch
+    def visit_AugAssign(self, node):
+        '''An augmented assign (contains an operator).'''
+        return (1, 2, (self.get_name(node.op),), (node.target, node.value))
+
+    @dispatch
+    def visit_Compare(self, node):
+        '''A comparison.'''
+        return (len(node.ops), len(node.comparators) + 1,
+                map(self.get_name, node.ops), node.comparators + [node.left])
+
+    def visit_FunctionDef(self, node):
+        '''When visiting functions, another visitor is created to recursively
+        analyze the function's body.
+        '''
+        for child in node.body:
+            visitor = HalsteadVisitor.from_ast(child, context=node.name)
+            self.operators += visitor.operators
+            self.operands += visitor.operands
+            self.operators_seen.update(visitor.operators_seen)
+            self.operands_seen.update(visitor.operands_seen)

eric ide

mercurial