|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Miscellaneous stuff for coverage.py.""" |
|
5 |
|
6 import contextlib |
|
7 import errno |
|
8 import hashlib |
|
9 import importlib |
|
10 import importlib.util |
|
11 import inspect |
|
12 import locale |
|
13 import os |
|
14 import os.path |
|
15 import re |
|
16 import sys |
|
17 import types |
|
18 |
|
19 from coverage import env |
|
20 from coverage.exceptions import CoverageException |
|
21 |
|
22 # In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of |
|
23 # other packages were importing the exceptions from misc, so import them here. |
|
24 # pylint: disable=unused-wildcard-import |
|
25 from coverage.exceptions import * # pylint: disable=wildcard-import |
|
26 |
|
27 ISOLATED_MODULES = {} |
|
28 |
|
29 |
|
30 def isolate_module(mod): |
|
31 """Copy a module so that we are isolated from aggressive mocking. |
|
32 |
|
33 If a test suite mocks os.path.exists (for example), and then we need to use |
|
34 it during the test, everything will get tangled up if we use their mock. |
|
35 Making a copy of the module when we import it will isolate coverage.py from |
|
36 those complications. |
|
37 """ |
|
38 if mod not in ISOLATED_MODULES: |
|
39 new_mod = types.ModuleType(mod.__name__) |
|
40 ISOLATED_MODULES[mod] = new_mod |
|
41 for name in dir(mod): |
|
42 value = getattr(mod, name) |
|
43 if isinstance(value, types.ModuleType): |
|
44 value = isolate_module(value) |
|
45 setattr(new_mod, name, value) |
|
46 return ISOLATED_MODULES[mod] |
|
47 |
|
48 os = isolate_module(os) |
|
49 |
|
50 |
|
51 class SysModuleSaver: |
|
52 """Saves the contents of sys.modules, and removes new modules later.""" |
|
53 def __init__(self): |
|
54 self.old_modules = set(sys.modules) |
|
55 |
|
56 def restore(self): |
|
57 """Remove any modules imported since this object started.""" |
|
58 new_modules = set(sys.modules) - self.old_modules |
|
59 for m in new_modules: |
|
60 del sys.modules[m] |
|
61 |
|
62 |
|
63 @contextlib.contextmanager |
|
64 def sys_modules_saved(): |
|
65 """A context manager to remove any modules imported during a block.""" |
|
66 saver = SysModuleSaver() |
|
67 try: |
|
68 yield |
|
69 finally: |
|
70 saver.restore() |
|
71 |
|
72 |
|
73 def import_third_party(modname): |
|
74 """Import a third-party module we need, but might not be installed. |
|
75 |
|
76 This also cleans out the module after the import, so that coverage won't |
|
77 appear to have imported it. This lets the third party use coverage for |
|
78 their own tests. |
|
79 |
|
80 Arguments: |
|
81 modname (str): the name of the module to import. |
|
82 |
|
83 Returns: |
|
84 The imported module, or None if the module couldn't be imported. |
|
85 |
|
86 """ |
|
87 with sys_modules_saved(): |
|
88 try: |
|
89 return importlib.import_module(modname) |
|
90 except ImportError: |
|
91 return None |
|
92 |
|
93 |
|
94 def dummy_decorator_with_args(*args_unused, **kwargs_unused): |
|
95 """Dummy no-op implementation of a decorator with arguments.""" |
|
96 def _decorator(func): |
|
97 return func |
|
98 return _decorator |
|
99 |
|
100 |
|
101 # Use PyContracts for assertion testing on parameters and returns, but only if |
|
102 # we are running our own test suite. |
|
103 if env.USE_CONTRACTS: |
|
104 from contracts import contract # pylint: disable=unused-import |
|
105 from contracts import new_contract as raw_new_contract |
|
106 |
|
107 def new_contract(*args, **kwargs): |
|
108 """A proxy for contracts.new_contract that doesn't mind happening twice.""" |
|
109 try: |
|
110 raw_new_contract(*args, **kwargs) |
|
111 except ValueError: |
|
112 # During meta-coverage, this module is imported twice, and |
|
113 # PyContracts doesn't like redefining contracts. It's OK. |
|
114 pass |
|
115 |
|
116 # Define contract words that PyContract doesn't have. |
|
117 new_contract('bytes', lambda v: isinstance(v, bytes)) |
|
118 new_contract('unicode', lambda v: isinstance(v, str)) |
|
119 |
|
120 def one_of(argnames): |
|
121 """Ensure that only one of the argnames is non-None.""" |
|
122 def _decorator(func): |
|
123 argnameset = {name.strip() for name in argnames.split(",")} |
|
124 def _wrapper(*args, **kwargs): |
|
125 vals = [kwargs.get(name) for name in argnameset] |
|
126 assert sum(val is not None for val in vals) == 1 |
|
127 return func(*args, **kwargs) |
|
128 return _wrapper |
|
129 return _decorator |
|
130 else: # pragma: not testing |
|
131 # We aren't using real PyContracts, so just define our decorators as |
|
132 # stunt-double no-ops. |
|
133 contract = dummy_decorator_with_args |
|
134 one_of = dummy_decorator_with_args |
|
135 |
|
136 def new_contract(*args_unused, **kwargs_unused): |
|
137 """Dummy no-op implementation of `new_contract`.""" |
|
138 pass |
|
139 |
|
140 |
|
141 def nice_pair(pair): |
|
142 """Make a nice string representation of a pair of numbers. |
|
143 |
|
144 If the numbers are equal, just return the number, otherwise return the pair |
|
145 with a dash between them, indicating the range. |
|
146 |
|
147 """ |
|
148 start, end = pair |
|
149 if start == end: |
|
150 return "%d" % start |
|
151 else: |
|
152 return "%d-%d" % (start, end) |
|
153 |
|
154 |
|
155 def expensive(fn): |
|
156 """A decorator to indicate that a method shouldn't be called more than once. |
|
157 |
|
158 Normally, this does nothing. During testing, this raises an exception if |
|
159 called more than once. |
|
160 |
|
161 """ |
|
162 if env.TESTING: |
|
163 attr = "_once_" + fn.__name__ |
|
164 |
|
165 def _wrapper(self): |
|
166 if hasattr(self, attr): |
|
167 raise AssertionError(f"Shouldn't have called {fn.__name__} more than once") |
|
168 setattr(self, attr, True) |
|
169 return fn(self) |
|
170 return _wrapper |
|
171 else: |
|
172 return fn # pragma: not testing |
|
173 |
|
174 |
|
175 def bool_or_none(b): |
|
176 """Return bool(b), but preserve None.""" |
|
177 if b is None: |
|
178 return None |
|
179 else: |
|
180 return bool(b) |
|
181 |
|
182 |
|
183 def join_regex(regexes): |
|
184 """Combine a list of regexes into one that matches any of them.""" |
|
185 return "|".join(f"(?:{r})" for r in regexes) |
|
186 |
|
187 |
|
188 def file_be_gone(path): |
|
189 """Remove a file, and don't get annoyed if it doesn't exist.""" |
|
190 try: |
|
191 os.remove(path) |
|
192 except OSError as e: |
|
193 if e.errno != errno.ENOENT: |
|
194 raise |
|
195 |
|
196 |
|
197 def ensure_dir(directory): |
|
198 """Make sure the directory exists. |
|
199 |
|
200 If `directory` is None or empty, do nothing. |
|
201 """ |
|
202 if directory: |
|
203 os.makedirs(directory, exist_ok=True) |
|
204 |
|
205 |
|
206 def ensure_dir_for_file(path): |
|
207 """Make sure the directory for the path exists.""" |
|
208 ensure_dir(os.path.dirname(path)) |
|
209 |
|
210 |
|
211 def output_encoding(outfile=None): |
|
212 """Determine the encoding to use for output written to `outfile` or stdout.""" |
|
213 if outfile is None: |
|
214 outfile = sys.stdout |
|
215 encoding = ( |
|
216 getattr(outfile, "encoding", None) or |
|
217 getattr(sys.__stdout__, "encoding", None) or |
|
218 locale.getpreferredencoding() |
|
219 ) |
|
220 return encoding |
|
221 |
|
222 |
|
223 class Hasher: |
|
224 """Hashes Python data for fingerprinting.""" |
|
225 def __init__(self): |
|
226 self.hash = hashlib.new("sha3_256") |
|
227 |
|
228 def update(self, v): |
|
229 """Add `v` to the hash, recursively if needed.""" |
|
230 self.hash.update(str(type(v)).encode("utf-8")) |
|
231 if isinstance(v, str): |
|
232 self.hash.update(v.encode("utf-8")) |
|
233 elif isinstance(v, bytes): |
|
234 self.hash.update(v) |
|
235 elif v is None: |
|
236 pass |
|
237 elif isinstance(v, (int, float)): |
|
238 self.hash.update(str(v).encode("utf-8")) |
|
239 elif isinstance(v, (tuple, list)): |
|
240 for e in v: |
|
241 self.update(e) |
|
242 elif isinstance(v, dict): |
|
243 keys = v.keys() |
|
244 for k in sorted(keys): |
|
245 self.update(k) |
|
246 self.update(v[k]) |
|
247 else: |
|
248 for k in dir(v): |
|
249 if k.startswith('__'): |
|
250 continue |
|
251 a = getattr(v, k) |
|
252 if inspect.isroutine(a): |
|
253 continue |
|
254 self.update(k) |
|
255 self.update(a) |
|
256 self.hash.update(b'.') |
|
257 |
|
258 def hexdigest(self): |
|
259 """Retrieve the hex digest of the hash.""" |
|
260 return self.hash.hexdigest()[:32] |
|
261 |
|
262 |
|
263 def _needs_to_implement(that, func_name): |
|
264 """Helper to raise NotImplementedError in interface stubs.""" |
|
265 if hasattr(that, "_coverage_plugin_name"): |
|
266 thing = "Plugin" |
|
267 name = that._coverage_plugin_name |
|
268 else: |
|
269 thing = "Class" |
|
270 klass = that.__class__ |
|
271 name = f"{klass.__module__}.{klass.__name__}" |
|
272 |
|
273 raise NotImplementedError( |
|
274 f"{thing} {name!r} needs to implement {func_name}()" |
|
275 ) |
|
276 |
|
277 |
|
278 class DefaultValue: |
|
279 """A sentinel object to use for unusual default-value needs. |
|
280 |
|
281 Construct with a string that will be used as the repr, for display in help |
|
282 and Sphinx output. |
|
283 |
|
284 """ |
|
285 def __init__(self, display_as): |
|
286 self.display_as = display_as |
|
287 |
|
288 def __repr__(self): |
|
289 return self.display_as |
|
290 |
|
291 |
|
292 def substitute_variables(text, variables): |
|
293 """Substitute ``${VAR}`` variables in `text` with their values. |
|
294 |
|
295 Variables in the text can take a number of shell-inspired forms:: |
|
296 |
|
297 $VAR |
|
298 ${VAR} |
|
299 ${VAR?} strict: an error if VAR isn't defined. |
|
300 ${VAR-missing} defaulted: "missing" if VAR isn't defined. |
|
301 $$ just a dollar sign. |
|
302 |
|
303 `variables` is a dictionary of variable values. |
|
304 |
|
305 Returns the resulting text with values substituted. |
|
306 |
|
307 """ |
|
308 dollar_pattern = r"""(?x) # Use extended regex syntax |
|
309 \$ # A dollar sign, |
|
310 (?: # then |
|
311 (?P<dollar>\$) | # a dollar sign, or |
|
312 (?P<word1>\w+) | # a plain word, or |
|
313 { # a {-wrapped |
|
314 (?P<word2>\w+) # word, |
|
315 (?: |
|
316 (?P<strict>\?) | # with a strict marker |
|
317 -(?P<defval>[^}]*) # or a default value |
|
318 )? # maybe. |
|
319 } |
|
320 ) |
|
321 """ |
|
322 |
|
323 dollar_groups = ('dollar', 'word1', 'word2') |
|
324 |
|
325 def dollar_replace(match): |
|
326 """Called for each $replacement.""" |
|
327 # Only one of the groups will have matched, just get its text. |
|
328 word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks |
|
329 if word == "$": |
|
330 return "$" |
|
331 elif word in variables: |
|
332 return variables[word] |
|
333 elif match.group('strict'): |
|
334 msg = f"Variable {word} is undefined: {text!r}" |
|
335 raise CoverageException(msg) |
|
336 else: |
|
337 return match.group('defval') |
|
338 |
|
339 text = re.sub(dollar_pattern, dollar_replace, text) |
|
340 return text |
|
341 |
|
342 |
|
343 def format_local_datetime(dt): |
|
344 """Return a string with local timezone representing the date. |
|
345 """ |
|
346 return dt.astimezone().strftime('%Y-%m-%d %H:%M %z') |
|
347 |
|
348 |
|
349 def import_local_file(modname, modfile=None): |
|
350 """Import a local file as a module. |
|
351 |
|
352 Opens a file in the current directory named `modname`.py, imports it |
|
353 as `modname`, and returns the module object. `modfile` is the file to |
|
354 import if it isn't in the current directory. |
|
355 |
|
356 """ |
|
357 if modfile is None: |
|
358 modfile = modname + '.py' |
|
359 spec = importlib.util.spec_from_file_location(modname, modfile) |
|
360 mod = importlib.util.module_from_spec(spec) |
|
361 sys.modules[modname] = mod |
|
362 spec.loader.exec_module(mod) |
|
363 |
|
364 return mod |
|
365 |
|
366 |
|
367 def human_key(s): |
|
368 """Turn a string into a list of string and number chunks. |
|
369 "z23a" -> ["z", 23, "a"] |
|
370 """ |
|
371 def tryint(s): |
|
372 """If `s` is a number, return an int, else `s` unchanged.""" |
|
373 try: |
|
374 return int(s) |
|
375 except ValueError: |
|
376 return s |
|
377 |
|
378 return [tryint(c) for c in re.split(r"(\d+)", s)] |
|
379 |
|
380 def human_sorted(strings): |
|
381 """Sort the given iterable of strings the way that humans expect. |
|
382 |
|
383 Numeric components in the strings are sorted as numbers. |
|
384 |
|
385 Returns the sorted list. |
|
386 |
|
387 """ |
|
388 return sorted(strings, key=human_key) |
|
389 |
|
390 def human_sorted_items(items, reverse=False): |
|
391 """Sort the (string, value) items the way humans expect. |
|
392 |
|
393 Returns the sorted list of items. |
|
394 """ |
|
395 return sorted(items, key=lambda pair: (human_key(pair[0]), pair[1]), reverse=reverse) |
|
396 |
|
397 |
|
398 def plural(n, thing="", things=""): |
|
399 """Pluralize a word. |
|
400 |
|
401 If n is 1, return thing. Otherwise return things, or thing+s. |
|
402 """ |
|
403 if n == 1: |
|
404 return thing |
|
405 else: |
|
406 return things or (thing + "s") |