1 """Miscellaneous stuff for Coverage.""" |
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.""" |
2 |
5 |
3 import errno |
6 import errno |
|
7 import hashlib |
4 import inspect |
8 import inspect |
5 import os |
9 import os |
6 import sys |
10 |
7 |
11 from coverage import env |
8 from .backward import md5, sorted # pylint: disable=W0622 |
12 from coverage.backward import string_class, to_bytes, unicode_class |
9 from .backward import string_class, to_bytes |
13 |
|
14 |
|
15 # Use PyContracts for assertion testing on parameters and returns, but only if |
|
16 # we are running our own test suite. |
|
17 if env.TESTING: |
|
18 from contracts import contract # pylint: disable=unused-import |
|
19 from contracts import new_contract |
|
20 |
|
21 try: |
|
22 # Define contract words that PyContract doesn't have. |
|
23 new_contract('bytes', lambda v: isinstance(v, bytes)) |
|
24 if env.PY3: |
|
25 new_contract('unicode', lambda v: isinstance(v, unicode_class)) |
|
26 except ValueError: |
|
27 # During meta-coverage, this module is imported twice, and PyContracts |
|
28 # doesn't like redefining contracts. It's OK. |
|
29 pass |
|
30 else: # pragma: not covered |
|
31 # We aren't using real PyContracts, so just define a no-op decorator as a |
|
32 # stunt double. |
|
33 def contract(**unused): |
|
34 """Dummy no-op implementation of `contract`.""" |
|
35 return lambda func: func |
10 |
36 |
11 |
37 |
12 def nice_pair(pair): |
38 def nice_pair(pair): |
13 """Make a nice string representation of a pair of numbers. |
39 """Make a nice string representation of a pair of numbers. |
14 |
40 |
54 pairs.append((start, end)) |
80 pairs.append((start, end)) |
55 ret = ', '.join(map(nice_pair, pairs)) |
81 ret = ', '.join(map(nice_pair, pairs)) |
56 return ret |
82 return ret |
57 |
83 |
58 |
84 |
59 def short_stack(): |
|
60 """Return a string summarizing the call stack.""" |
|
61 stack = inspect.stack()[:0:-1] |
|
62 return "\n".join(["%30s : %s @%d" % (t[3],t[1],t[2]) for t in stack]) |
|
63 |
|
64 |
|
65 def expensive(fn): |
85 def expensive(fn): |
66 """A decorator to cache the result of an expensive operation. |
86 """A decorator to indicate that a method shouldn't be called more than once. |
67 |
87 |
68 Only applies to methods with no arguments. |
88 Normally, this does nothing. During testing, this raises an exception if |
69 |
89 called more than once. |
70 """ |
90 |
71 attr = "_cache_" + fn.__name__ |
91 """ |
72 def _wrapped(self): |
92 if env.TESTING: |
73 """Inner fn that checks the cache.""" |
93 attr = "_once_" + fn.__name__ |
74 if not hasattr(self, attr): |
94 |
75 setattr(self, attr, fn(self)) |
95 def _wrapped(self): |
76 return getattr(self, attr) |
96 """Inner function that checks the cache.""" |
77 return _wrapped |
97 if hasattr(self, attr): |
|
98 raise Exception("Shouldn't have called %s more than once" % fn.__name__) |
|
99 setattr(self, attr, True) |
|
100 return fn(self) |
|
101 return _wrapped |
|
102 else: |
|
103 return fn |
78 |
104 |
79 |
105 |
80 def bool_or_none(b): |
106 def bool_or_none(b): |
81 """Return bool(b), but preserve None.""" |
107 """Return bool(b), but preserve None.""" |
82 if b is None: |
108 if b is None: |
85 return bool(b) |
111 return bool(b) |
86 |
112 |
87 |
113 |
88 def join_regex(regexes): |
114 def join_regex(regexes): |
89 """Combine a list of regexes into one that matches any of them.""" |
115 """Combine a list of regexes into one that matches any of them.""" |
90 if len(regexes) > 1: |
116 return "|".join("(?:%s)" % r for r in regexes) |
91 return "|".join(["(%s)" % r for r in regexes]) |
|
92 elif regexes: |
|
93 return regexes[0] |
|
94 else: |
|
95 return "" |
|
96 |
117 |
97 |
118 |
98 def file_be_gone(path): |
119 def file_be_gone(path): |
99 """Remove a file, and don't get annoyed if it doesn't exist.""" |
120 """Remove a file, and don't get annoyed if it doesn't exist.""" |
100 try: |
121 try: |
101 os.remove(path) |
122 os.remove(path) |
102 except OSError: |
123 except OSError as e: |
103 _, e, _ = sys.exc_info() |
|
104 if e.errno != errno.ENOENT: |
124 if e.errno != errno.ENOENT: |
105 raise |
125 raise |
106 |
126 |
107 |
127 |
108 class Hasher(object): |
128 class Hasher(object): |
109 """Hashes Python data into md5.""" |
129 """Hashes Python data into md5.""" |
110 def __init__(self): |
130 def __init__(self): |
111 self.md5 = md5() |
131 self.md5 = hashlib.md5() |
112 |
132 |
113 def update(self, v): |
133 def update(self, v): |
114 """Add `v` to the hash, recursively if needed.""" |
134 """Add `v` to the hash, recursively if needed.""" |
115 self.md5.update(to_bytes(str(type(v)))) |
135 self.md5.update(to_bytes(str(type(v)))) |
116 if isinstance(v, string_class): |
136 if isinstance(v, string_class): |
117 self.md5.update(to_bytes(v)) |
137 self.md5.update(to_bytes(v)) |
|
138 elif isinstance(v, bytes): |
|
139 self.md5.update(v) |
118 elif v is None: |
140 elif v is None: |
119 pass |
141 pass |
120 elif isinstance(v, (int, float)): |
142 elif isinstance(v, (int, float)): |
121 self.md5.update(to_bytes(str(v))) |
143 self.md5.update(to_bytes(str(v))) |
122 elif isinstance(v, (tuple, list)): |
144 elif isinstance(v, (tuple, list)): |
135 if inspect.isroutine(a): |
157 if inspect.isroutine(a): |
136 continue |
158 continue |
137 self.update(k) |
159 self.update(k) |
138 self.update(a) |
160 self.update(a) |
139 |
161 |
140 def digest(self): |
162 def hexdigest(self): |
141 """Retrieve the digest of the hash.""" |
163 """Retrieve the hex digest of the hash.""" |
142 return self.md5.digest() |
164 return self.md5.hexdigest() |
|
165 |
|
166 |
|
167 def _needs_to_implement(that, func_name): |
|
168 """Helper to raise NotImplementedError in interface stubs.""" |
|
169 if hasattr(that, "_coverage_plugin_name"): |
|
170 thing = "Plugin" |
|
171 name = that._coverage_plugin_name |
|
172 else: |
|
173 thing = "Class" |
|
174 klass = that.__class__ |
|
175 name = "{klass.__module__}.{klass.__name__}".format(klass=klass) |
|
176 |
|
177 raise NotImplementedError( |
|
178 "{thing} {name!r} needs to implement {func_name}()".format( |
|
179 thing=thing, name=name, func_name=func_name |
|
180 ) |
|
181 ) |
143 |
182 |
144 |
183 |
145 class CoverageException(Exception): |
184 class CoverageException(Exception): |
146 """An exception specific to Coverage.""" |
185 """An exception specific to coverage.py.""" |
147 pass |
186 pass |
|
187 |
148 |
188 |
149 class NoSource(CoverageException): |
189 class NoSource(CoverageException): |
150 """We couldn't find the source for a module.""" |
190 """We couldn't find the source for a module.""" |
151 pass |
191 pass |
152 |
192 |
|
193 |
153 class NoCode(NoSource): |
194 class NoCode(NoSource): |
154 """We couldn't find any code at all.""" |
195 """We couldn't find any code at all.""" |
155 pass |
196 pass |
156 |
197 |
|
198 |
157 class NotPython(CoverageException): |
199 class NotPython(CoverageException): |
158 """A source file turned out not to be parsable Python.""" |
200 """A source file turned out not to be parsable Python.""" |
159 pass |
201 pass |
160 |
202 |
|
203 |
161 class ExceptionDuringRun(CoverageException): |
204 class ExceptionDuringRun(CoverageException): |
162 """An exception happened while running customer code. |
205 """An exception happened while running customer code. |
163 |
206 |
164 Construct it with three arguments, the values from `sys.exc_info`. |
207 Construct it with three arguments, the values from `sys.exc_info`. |
165 |
208 |