Home | History | Annotate | Download | only in coverage
      1 """Miscellaneous stuff for Coverage."""
      2 
      3 import inspect
      4 from coverage.backward import md5, sorted       # pylint: disable=W0622
      5 from coverage.backward import string_class, to_bytes
      6 
      7 
      8 def nice_pair(pair):
      9     """Make a nice string representation of a pair of numbers.
     10 
     11     If the numbers are equal, just return the number, otherwise return the pair
     12     with a dash between them, indicating the range.
     13 
     14     """
     15     start, end = pair
     16     if start == end:
     17         return "%d" % start
     18     else:
     19         return "%d-%d" % (start, end)
     20 
     21 
     22 def format_lines(statements, lines):
     23     """Nicely format a list of line numbers.
     24 
     25     Format a list of line numbers for printing by coalescing groups of lines as
     26     long as the lines represent consecutive statements.  This will coalesce
     27     even if there are gaps between statements.
     28 
     29     For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and
     30     `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14".
     31 
     32     """
     33     pairs = []
     34     i = 0
     35     j = 0
     36     start = None
     37     while i < len(statements) and j < len(lines):
     38         if statements[i] == lines[j]:
     39             if start == None:
     40                 start = lines[j]
     41             end = lines[j]
     42             j += 1
     43         elif start:
     44             pairs.append((start, end))
     45             start = None
     46         i += 1
     47     if start:
     48         pairs.append((start, end))
     49     ret = ', '.join(map(nice_pair, pairs))
     50     return ret
     51 
     52 
     53 def expensive(fn):
     54     """A decorator to cache the result of an expensive operation.
     55 
     56     Only applies to methods with no arguments.
     57 
     58     """
     59     attr = "_cache_" + fn.__name__
     60     def _wrapped(self):
     61         """Inner fn that checks the cache."""
     62         if not hasattr(self, attr):
     63             setattr(self, attr, fn(self))
     64         return getattr(self, attr)
     65     return _wrapped
     66 
     67 
     68 def bool_or_none(b):
     69     """Return bool(b), but preserve None."""
     70     if b is None:
     71         return None
     72     else:
     73         return bool(b)
     74 
     75 
     76 def join_regex(regexes):
     77     """Combine a list of regexes into one that matches any of them."""
     78     if len(regexes) > 1:
     79         return "(" + ")|(".join(regexes) + ")"
     80     elif regexes:
     81         return regexes[0]
     82     else:
     83         return ""
     84 
     85 
     86 class Hasher(object):
     87     """Hashes Python data into md5."""
     88     def __init__(self):
     89         self.md5 = md5()
     90 
     91     def update(self, v):
     92         """Add `v` to the hash, recursively if needed."""
     93         self.md5.update(to_bytes(str(type(v))))
     94         if isinstance(v, string_class):
     95             self.md5.update(to_bytes(v))
     96         elif isinstance(v, (int, float)):
     97             self.update(str(v))
     98         elif isinstance(v, (tuple, list)):
     99             for e in v:
    100                 self.update(e)
    101         elif isinstance(v, dict):
    102             keys = v.keys()
    103             for k in sorted(keys):
    104                 self.update(k)
    105                 self.update(v[k])
    106         else:
    107             for k in dir(v):
    108                 if k.startswith('__'):
    109                     continue
    110                 a = getattr(v, k)
    111                 if inspect.isroutine(a):
    112                     continue
    113                 self.update(k)
    114                 self.update(a)
    115 
    116     def digest(self):
    117         """Retrieve the digest of the hash."""
    118         return self.md5.digest()
    119 
    120 
    121 class CoverageException(Exception):
    122     """An exception specific to Coverage."""
    123     pass
    124 
    125 class NoSource(CoverageException):
    126     """We couldn't find the source for a module."""
    127     pass
    128 
    129 class NotPython(CoverageException):
    130     """A source file turned out not to be parsable Python."""
    131     pass
    132 
    133 class ExceptionDuringRun(CoverageException):
    134     """An exception happened while running customer code.
    135 
    136     Construct it with three arguments, the values from `sys.exc_info`.
    137 
    138     """
    139     pass
    140