Sun, 13 Sep 2015 17:56:31 +0200
Added the radon files.
--- /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)