blob: fd9be8572a5bc081a1774b36db815bb4813b9289 [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001"""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