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