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