1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4"""Miscellaneous stuff for coverage.py."""
5
6import errno
7import hashlib
8import inspect
9import locale
10import os
11import sys
12import types
13
14from coverage import env
15from coverage.backward import string_class, to_bytes, unicode_class
16
17ISOLATED_MODULES = {}
18
19
20def isolate_module(mod):
21    """Copy a module so that we are isolated from aggressive mocking.
22
23    If a test suite mocks os.path.exists (for example), and then we need to use
24    it during the test, everything will get tangled up if we use their mock.
25    Making a copy of the module when we import it will isolate coverage.py from
26    those complications.
27    """
28    if mod not in ISOLATED_MODULES:
29        new_mod = types.ModuleType(mod.__name__)
30        ISOLATED_MODULES[mod] = new_mod
31        for name in dir(mod):
32            value = getattr(mod, name)
33            if isinstance(value, types.ModuleType):
34                value = isolate_module(value)
35            setattr(new_mod, name, value)
36    return ISOLATED_MODULES[mod]
37
38os = isolate_module(os)
39
40
41# Use PyContracts for assertion testing on parameters and returns, but only if
42# we are running our own test suite.
43if env.TESTING:
44    from contracts import contract              # pylint: disable=unused-import
45    from contracts import new_contract
46
47    try:
48        # Define contract words that PyContract doesn't have.
49        new_contract('bytes', lambda v: isinstance(v, bytes))
50        if env.PY3:
51            new_contract('unicode', lambda v: isinstance(v, unicode_class))
52    except ValueError:
53        # During meta-coverage, this module is imported twice, and PyContracts
54        # doesn't like redefining contracts. It's OK.
55        pass
56else:                                           # pragma: not covered
57    # We aren't using real PyContracts, so just define a no-op decorator as a
58    # stunt double.
59    def contract(**unused):
60        """Dummy no-op implementation of `contract`."""
61        return lambda func: func
62
63
64def nice_pair(pair):
65    """Make a nice string representation of a pair of numbers.
66
67    If the numbers are equal, just return the number, otherwise return the pair
68    with a dash between them, indicating the range.
69
70    """
71    start, end = pair
72    if start == end:
73        return "%d" % start
74    else:
75        return "%d-%d" % (start, end)
76
77
78def format_lines(statements, lines):
79    """Nicely format a list of line numbers.
80
81    Format a list of line numbers for printing by coalescing groups of lines as
82    long as the lines represent consecutive statements.  This will coalesce
83    even if there are gaps between statements.
84
85    For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and
86    `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14".
87
88    """
89    pairs = []
90    i = 0
91    j = 0
92    start = None
93    statements = sorted(statements)
94    lines = sorted(lines)
95    while i < len(statements) and j < len(lines):
96        if statements[i] == lines[j]:
97            if start is None:
98                start = lines[j]
99            end = lines[j]
100            j += 1
101        elif start:
102            pairs.append((start, end))
103            start = None
104        i += 1
105    if start:
106        pairs.append((start, end))
107    ret = ', '.join(map(nice_pair, pairs))
108    return ret
109
110
111def expensive(fn):
112    """A decorator to indicate that a method shouldn't be called more than once.
113
114    Normally, this does nothing.  During testing, this raises an exception if
115    called more than once.
116
117    """
118    if env.TESTING:
119        attr = "_once_" + fn.__name__
120
121        def _wrapped(self):
122            """Inner function that checks the cache."""
123            if hasattr(self, attr):
124                raise Exception("Shouldn't have called %s more than once" % fn.__name__)
125            setattr(self, attr, True)
126            return fn(self)
127        return _wrapped
128    else:
129        return fn
130
131
132def bool_or_none(b):
133    """Return bool(b), but preserve None."""
134    if b is None:
135        return None
136    else:
137        return bool(b)
138
139
140def join_regex(regexes):
141    """Combine a list of regexes into one that matches any of them."""
142    return "|".join("(?:%s)" % r for r in regexes)
143
144
145def file_be_gone(path):
146    """Remove a file, and don't get annoyed if it doesn't exist."""
147    try:
148        os.remove(path)
149    except OSError as e:
150        if e.errno != errno.ENOENT:
151            raise
152
153
154def output_encoding(outfile=None):
155    """Determine the encoding to use for output written to `outfile` or stdout."""
156    if outfile is None:
157        outfile = sys.stdout
158    encoding = (
159        getattr(outfile, "encoding", None) or
160        getattr(sys.__stdout__, "encoding", None) or
161        locale.getpreferredencoding()
162    )
163    return encoding
164
165
166class Hasher(object):
167    """Hashes Python data into md5."""
168    def __init__(self):
169        self.md5 = hashlib.md5()
170
171    def update(self, v):
172        """Add `v` to the hash, recursively if needed."""
173        self.md5.update(to_bytes(str(type(v))))
174        if isinstance(v, string_class):
175            self.md5.update(to_bytes(v))
176        elif isinstance(v, bytes):
177            self.md5.update(v)
178        elif v is None:
179            pass
180        elif isinstance(v, (int, float)):
181            self.md5.update(to_bytes(str(v)))
182        elif isinstance(v, (tuple, list)):
183            for e in v:
184                self.update(e)
185        elif isinstance(v, dict):
186            keys = v.keys()
187            for k in sorted(keys):
188                self.update(k)
189                self.update(v[k])
190        else:
191            for k in dir(v):
192                if k.startswith('__'):
193                    continue
194                a = getattr(v, k)
195                if inspect.isroutine(a):
196                    continue
197                self.update(k)
198                self.update(a)
199
200    def hexdigest(self):
201        """Retrieve the hex digest of the hash."""
202        return self.md5.hexdigest()
203
204
205def _needs_to_implement(that, func_name):
206    """Helper to raise NotImplementedError in interface stubs."""
207    if hasattr(that, "_coverage_plugin_name"):
208        thing = "Plugin"
209        name = that._coverage_plugin_name
210    else:
211        thing = "Class"
212        klass = that.__class__
213        name = "{klass.__module__}.{klass.__name__}".format(klass=klass)
214
215    raise NotImplementedError(
216        "{thing} {name!r} needs to implement {func_name}()".format(
217            thing=thing, name=name, func_name=func_name
218            )
219        )
220
221
222class CoverageException(Exception):
223    """An exception specific to coverage.py."""
224    pass
225
226
227class NoSource(CoverageException):
228    """We couldn't find the source for a module."""
229    pass
230
231
232class NoCode(NoSource):
233    """We couldn't find any code at all."""
234    pass
235
236
237class NotPython(CoverageException):
238    """A source file turned out not to be parsable Python."""
239    pass
240
241
242class ExceptionDuringRun(CoverageException):
243    """An exception happened while running customer code.
244
245    Construct it with three arguments, the values from `sys.exc_info`.
246
247    """
248    pass
249