1"""Miscellaneous stuff for Coverage.""" 2 3import inspect 4from coverage.backward import md5, sorted # pylint: disable=W0622 5from coverage.backward import string_class, to_bytes 6 7 8def 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 22def 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 53def 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 68def 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 76def 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 86class 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 121class CoverageException(Exception): 122 """An exception specific to Coverage.""" 123 pass 124 125class NoSource(CoverageException): 126 """We couldn't find the source for a module.""" 127 pass 128 129class NotPython(CoverageException): 130 """A source file turned out not to be parsable Python.""" 131 pass 132 133class 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