|
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 """Determining whether files are being measured/reported or not.""" |
|
5 |
|
6 import importlib.util |
|
7 import inspect |
|
8 import itertools |
|
9 import os |
|
10 import platform |
|
11 import re |
|
12 import sys |
|
13 import sysconfig |
|
14 import traceback |
|
15 |
|
16 from coverage import env |
|
17 from coverage.disposition import FileDisposition, disposition_init |
|
18 from coverage.exceptions import CoverageException, PluginError |
|
19 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher |
|
20 from coverage.files import prep_patterns, find_python_files, canonical_filename |
|
21 from coverage.misc import sys_modules_saved |
|
22 from coverage.python import source_for_file, source_for_morf |
|
23 |
|
24 |
|
25 # Pypy has some unusual stuff in the "stdlib". Consider those locations |
|
26 # when deciding where the stdlib is. These modules are not used for anything, |
|
27 # they are modules importable from the pypy lib directories, so that we can |
|
28 # find those directories. |
|
29 _structseq = _pypy_irc_topic = None |
|
30 if env.PYPY: |
|
31 try: |
|
32 import _structseq |
|
33 except ImportError: |
|
34 pass |
|
35 |
|
36 try: |
|
37 import _pypy_irc_topic |
|
38 except ImportError: |
|
39 pass |
|
40 |
|
41 |
|
42 def canonical_path(morf, directory=False): |
|
43 """Return the canonical path of the module or file `morf`. |
|
44 |
|
45 If the module is a package, then return its directory. If it is a |
|
46 module, then return its file, unless `directory` is True, in which |
|
47 case return its enclosing directory. |
|
48 |
|
49 """ |
|
50 morf_path = canonical_filename(source_for_morf(morf)) |
|
51 if morf_path.endswith("__init__.py") or directory: |
|
52 morf_path = os.path.split(morf_path)[0] |
|
53 return morf_path |
|
54 |
|
55 |
|
56 def name_for_module(filename, frame): |
|
57 """Get the name of the module for a filename and frame. |
|
58 |
|
59 For configurability's sake, we allow __main__ modules to be matched by |
|
60 their importable name. |
|
61 |
|
62 If loaded via runpy (aka -m), we can usually recover the "original" |
|
63 full dotted module name, otherwise, we resort to interpreting the |
|
64 file name to get the module's name. In the case that the module name |
|
65 can't be determined, None is returned. |
|
66 |
|
67 """ |
|
68 module_globals = frame.f_globals if frame is not None else {} |
|
69 if module_globals is None: # pragma: only ironpython |
|
70 # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 |
|
71 module_globals = {} |
|
72 |
|
73 dunder_name = module_globals.get('__name__', None) |
|
74 |
|
75 if isinstance(dunder_name, str) and dunder_name != '__main__': |
|
76 # This is the usual case: an imported module. |
|
77 return dunder_name |
|
78 |
|
79 loader = module_globals.get('__loader__', None) |
|
80 for attrname in ('fullname', 'name'): # attribute renamed in py3.2 |
|
81 if hasattr(loader, attrname): |
|
82 fullname = getattr(loader, attrname) |
|
83 else: |
|
84 continue |
|
85 |
|
86 if isinstance(fullname, str) and fullname != '__main__': |
|
87 # Module loaded via: runpy -m |
|
88 return fullname |
|
89 |
|
90 # Script as first argument to Python command line. |
|
91 inspectedname = inspect.getmodulename(filename) |
|
92 if inspectedname is not None: |
|
93 return inspectedname |
|
94 else: |
|
95 return dunder_name |
|
96 |
|
97 |
|
98 def module_is_namespace(mod): |
|
99 """Is the module object `mod` a PEP420 namespace module?""" |
|
100 return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None |
|
101 |
|
102 |
|
103 def module_has_file(mod): |
|
104 """Does the module object `mod` have an existing __file__ ?""" |
|
105 mod__file__ = getattr(mod, '__file__', None) |
|
106 if mod__file__ is None: |
|
107 return False |
|
108 return os.path.exists(mod__file__) |
|
109 |
|
110 |
|
111 def file_and_path_for_module(modulename): |
|
112 """Find the file and search path for `modulename`. |
|
113 |
|
114 Returns: |
|
115 filename: The filename of the module, or None. |
|
116 path: A list (possibly empty) of directories to find submodules in. |
|
117 |
|
118 """ |
|
119 filename = None |
|
120 path = [] |
|
121 try: |
|
122 spec = importlib.util.find_spec(modulename) |
|
123 except Exception: |
|
124 pass |
|
125 else: |
|
126 if spec is not None: |
|
127 filename = spec.origin |
|
128 path = list(spec.submodule_search_locations or ()) |
|
129 return filename, path |
|
130 |
|
131 |
|
132 def add_stdlib_paths(paths): |
|
133 """Add paths where the stdlib can be found to the set `paths`.""" |
|
134 # Look at where some standard modules are located. That's the |
|
135 # indication for "installed with the interpreter". In some |
|
136 # environments (virtualenv, for example), these modules may be |
|
137 # spread across a few locations. Look at all the candidate modules |
|
138 # we've imported, and take all the different ones. |
|
139 modules_we_happen_to_have = [ |
|
140 inspect, itertools, os, platform, re, sysconfig, traceback, |
|
141 _pypy_irc_topic, _structseq, |
|
142 ] |
|
143 for m in modules_we_happen_to_have: |
|
144 if m is not None and hasattr(m, "__file__"): |
|
145 paths.add(canonical_path(m, directory=True)) |
|
146 |
|
147 if _structseq and not hasattr(_structseq, '__file__'): |
|
148 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
|
149 # objects still have the file names. So dig into one to find |
|
150 # the path to exclude. The "filename" might be synthetic, |
|
151 # don't be fooled by those. |
|
152 structseq_file = _structseq.structseq_new.__code__.co_filename |
|
153 if not structseq_file.startswith("<"): |
|
154 paths.add(canonical_path(structseq_file)) |
|
155 |
|
156 |
|
157 def add_third_party_paths(paths): |
|
158 """Add locations for third-party packages to the set `paths`.""" |
|
159 # Get the paths that sysconfig knows about. |
|
160 scheme_names = set(sysconfig.get_scheme_names()) |
|
161 |
|
162 for scheme in scheme_names: |
|
163 # https://foss.heptapod.net/pypy/pypy/-/issues/3433 |
|
164 better_scheme = "pypy_posix" if scheme == "pypy" else scheme |
|
165 if os.name in better_scheme.split("_"): |
|
166 config_paths = sysconfig.get_paths(scheme) |
|
167 for path_name in ["platlib", "purelib", "scripts"]: |
|
168 paths.add(config_paths[path_name]) |
|
169 |
|
170 |
|
171 def add_coverage_paths(paths): |
|
172 """Add paths where coverage.py code can be found to the set `paths`.""" |
|
173 cover_path = canonical_path(__file__, directory=True) |
|
174 paths.add(cover_path) |
|
175 if env.TESTING: |
|
176 # Don't include our own test code. |
|
177 paths.add(os.path.join(cover_path, "tests")) |
|
178 |
|
179 # When testing, we use PyContracts, which should be considered |
|
180 # part of coverage.py, and it uses six. Exclude those directories |
|
181 # just as we exclude ourselves. |
|
182 if env.USE_CONTRACTS: |
|
183 import contracts |
|
184 import six |
|
185 for mod in [contracts, six]: |
|
186 paths.add(canonical_path(mod)) |
|
187 |
|
188 |
|
189 class InOrOut: |
|
190 """Machinery for determining what files to measure.""" |
|
191 |
|
192 def __init__(self, warn, debug): |
|
193 self.warn = warn |
|
194 self.debug = debug |
|
195 |
|
196 # The matchers for should_trace. |
|
197 self.source_match = None |
|
198 self.source_pkgs_match = None |
|
199 self.pylib_paths = self.cover_paths = self.third_paths = None |
|
200 self.pylib_match = self.cover_match = self.third_match = None |
|
201 self.include_match = self.omit_match = None |
|
202 self.plugins = [] |
|
203 self.disp_class = FileDisposition |
|
204 |
|
205 # The source argument can be directories or package names. |
|
206 self.source = [] |
|
207 self.source_pkgs = [] |
|
208 self.source_pkgs_unmatched = [] |
|
209 self.omit = self.include = None |
|
210 |
|
211 # Is the source inside a third-party area? |
|
212 self.source_in_third = False |
|
213 |
|
214 def configure(self, config): |
|
215 """Apply the configuration to get ready for decision-time.""" |
|
216 self.source_pkgs.extend(config.source_pkgs) |
|
217 for src in config.source or []: |
|
218 if os.path.isdir(src): |
|
219 self.source.append(canonical_filename(src)) |
|
220 else: |
|
221 self.source_pkgs.append(src) |
|
222 self.source_pkgs_unmatched = self.source_pkgs[:] |
|
223 |
|
224 self.omit = prep_patterns(config.run_omit) |
|
225 self.include = prep_patterns(config.run_include) |
|
226 |
|
227 # The directories for files considered "installed with the interpreter". |
|
228 self.pylib_paths = set() |
|
229 if not config.cover_pylib: |
|
230 add_stdlib_paths(self.pylib_paths) |
|
231 |
|
232 # To avoid tracing the coverage.py code itself, we skip anything |
|
233 # located where we are. |
|
234 self.cover_paths = set() |
|
235 add_coverage_paths(self.cover_paths) |
|
236 |
|
237 # Find where third-party packages are installed. |
|
238 self.third_paths = set() |
|
239 add_third_party_paths(self.third_paths) |
|
240 |
|
241 def debug(msg): |
|
242 if self.debug: |
|
243 self.debug.write(msg) |
|
244 |
|
245 # Generally useful information |
|
246 debug("sys.path:" + "".join(f"\n {p}" for p in sys.path)) |
|
247 |
|
248 # Create the matchers we need for should_trace |
|
249 if self.source or self.source_pkgs: |
|
250 against = [] |
|
251 if self.source: |
|
252 self.source_match = TreeMatcher(self.source, "source") |
|
253 against.append(f"trees {self.source_match!r}") |
|
254 if self.source_pkgs: |
|
255 self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs") |
|
256 against.append(f"modules {self.source_pkgs_match!r}") |
|
257 debug("Source matching against " + " and ".join(against)) |
|
258 else: |
|
259 if self.pylib_paths: |
|
260 self.pylib_match = TreeMatcher(self.pylib_paths, "pylib") |
|
261 debug(f"Python stdlib matching: {self.pylib_match!r}") |
|
262 if self.include: |
|
263 self.include_match = FnmatchMatcher(self.include, "include") |
|
264 debug(f"Include matching: {self.include_match!r}") |
|
265 if self.omit: |
|
266 self.omit_match = FnmatchMatcher(self.omit, "omit") |
|
267 debug(f"Omit matching: {self.omit_match!r}") |
|
268 |
|
269 self.cover_match = TreeMatcher(self.cover_paths, "coverage") |
|
270 debug(f"Coverage code matching: {self.cover_match!r}") |
|
271 |
|
272 self.third_match = TreeMatcher(self.third_paths, "third") |
|
273 debug(f"Third-party lib matching: {self.third_match!r}") |
|
274 |
|
275 # Check if the source we want to measure has been installed as a |
|
276 # third-party package. |
|
277 with sys_modules_saved(): |
|
278 for pkg in self.source_pkgs: |
|
279 try: |
|
280 modfile, path = file_and_path_for_module(pkg) |
|
281 debug(f"Imported source package {pkg!r} as {modfile!r}") |
|
282 except CoverageException as exc: |
|
283 debug(f"Couldn't import source package {pkg!r}: {exc}") |
|
284 continue |
|
285 if modfile: |
|
286 if self.third_match.match(modfile): |
|
287 debug( |
|
288 f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}" |
|
289 ) |
|
290 self.source_in_third = True |
|
291 else: |
|
292 for pathdir in path: |
|
293 if self.third_match.match(pathdir): |
|
294 debug( |
|
295 f"Source is in third-party because of {pkg!r} path directory " + |
|
296 f"at {pathdir!r}" |
|
297 ) |
|
298 self.source_in_third = True |
|
299 |
|
300 for src in self.source: |
|
301 if self.third_match.match(src): |
|
302 debug(f"Source is in third-party because of source directory {src!r}") |
|
303 self.source_in_third = True |
|
304 |
|
305 def should_trace(self, filename, frame=None): |
|
306 """Decide whether to trace execution in `filename`, with a reason. |
|
307 |
|
308 This function is called from the trace function. As each new file name |
|
309 is encountered, this function determines whether it is traced or not. |
|
310 |
|
311 Returns a FileDisposition object. |
|
312 |
|
313 """ |
|
314 original_filename = filename |
|
315 disp = disposition_init(self.disp_class, filename) |
|
316 |
|
317 def nope(disp, reason): |
|
318 """Simple helper to make it easy to return NO.""" |
|
319 disp.trace = False |
|
320 disp.reason = reason |
|
321 return disp |
|
322 |
|
323 if original_filename.startswith('<'): |
|
324 return nope(disp, "not a real original file name") |
|
325 |
|
326 if frame is not None: |
|
327 # Compiled Python files have two file names: frame.f_code.co_filename is |
|
328 # the file name at the time the .pyc was compiled. The second name is |
|
329 # __file__, which is where the .pyc was actually loaded from. Since |
|
330 # .pyc files can be moved after compilation (for example, by being |
|
331 # installed), we look for __file__ in the frame and prefer it to the |
|
332 # co_filename value. |
|
333 dunder_file = frame.f_globals and frame.f_globals.get('__file__') |
|
334 if dunder_file: |
|
335 filename = source_for_file(dunder_file) |
|
336 if original_filename and not original_filename.startswith('<'): |
|
337 orig = os.path.basename(original_filename) |
|
338 if orig != os.path.basename(filename): |
|
339 # Files shouldn't be renamed when moved. This happens when |
|
340 # exec'ing code. If it seems like something is wrong with |
|
341 # the frame's file name, then just use the original. |
|
342 filename = original_filename |
|
343 |
|
344 if not filename: |
|
345 # Empty string is pretty useless. |
|
346 return nope(disp, "empty string isn't a file name") |
|
347 |
|
348 if filename.startswith('memory:'): |
|
349 return nope(disp, "memory isn't traceable") |
|
350 |
|
351 if filename.startswith('<'): |
|
352 # Lots of non-file execution is represented with artificial |
|
353 # file names like "<string>", "<doctest readme.txt[0]>", or |
|
354 # "<exec_function>". Don't ever trace these executions, since we |
|
355 # can't do anything with the data later anyway. |
|
356 return nope(disp, "not a real file name") |
|
357 |
|
358 # Jython reports the .class file to the tracer, use the source file. |
|
359 if filename.endswith("$py.class"): |
|
360 filename = filename[:-9] + ".py" |
|
361 |
|
362 canonical = canonical_filename(filename) |
|
363 disp.canonical_filename = canonical |
|
364 |
|
365 # Try the plugins, see if they have an opinion about the file. |
|
366 plugin = None |
|
367 for plugin in self.plugins.file_tracers: |
|
368 if not plugin._coverage_enabled: |
|
369 continue |
|
370 |
|
371 try: |
|
372 file_tracer = plugin.file_tracer(canonical) |
|
373 if file_tracer is not None: |
|
374 file_tracer._coverage_plugin = plugin |
|
375 disp.trace = True |
|
376 disp.file_tracer = file_tracer |
|
377 if file_tracer.has_dynamic_source_filename(): |
|
378 disp.has_dynamic_filename = True |
|
379 else: |
|
380 disp.source_filename = canonical_filename( |
|
381 file_tracer.source_filename() |
|
382 ) |
|
383 break |
|
384 except Exception: |
|
385 plugin_name = plugin._coverage_plugin_name |
|
386 tb = traceback.format_exc() |
|
387 self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}") |
|
388 plugin._coverage_enabled = False |
|
389 continue |
|
390 else: |
|
391 # No plugin wanted it: it's Python. |
|
392 disp.trace = True |
|
393 disp.source_filename = canonical |
|
394 |
|
395 if not disp.has_dynamic_filename: |
|
396 if not disp.source_filename: |
|
397 raise PluginError( |
|
398 f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'" |
|
399 ) |
|
400 reason = self.check_include_omit_etc(disp.source_filename, frame) |
|
401 if reason: |
|
402 nope(disp, reason) |
|
403 |
|
404 return disp |
|
405 |
|
406 def check_include_omit_etc(self, filename, frame): |
|
407 """Check a file name against the include, omit, etc, rules. |
|
408 |
|
409 Returns a string or None. String means, don't trace, and is the reason |
|
410 why. None means no reason found to not trace. |
|
411 |
|
412 """ |
|
413 modulename = name_for_module(filename, frame) |
|
414 |
|
415 # If the user specified source or include, then that's authoritative |
|
416 # about the outer bound of what to measure and we don't have to apply |
|
417 # any canned exclusions. If they didn't, then we have to exclude the |
|
418 # stdlib and coverage.py directories. |
|
419 if self.source_match or self.source_pkgs_match: |
|
420 extra = "" |
|
421 ok = False |
|
422 if self.source_pkgs_match: |
|
423 if self.source_pkgs_match.match(modulename): |
|
424 ok = True |
|
425 if modulename in self.source_pkgs_unmatched: |
|
426 self.source_pkgs_unmatched.remove(modulename) |
|
427 else: |
|
428 extra = f"module {modulename!r} " |
|
429 if not ok and self.source_match: |
|
430 if self.source_match.match(filename): |
|
431 ok = True |
|
432 if not ok: |
|
433 return extra + "falls outside the --source spec" |
|
434 if not self.source_in_third: |
|
435 if self.third_match.match(filename): |
|
436 return "inside --source, but is third-party" |
|
437 elif self.include_match: |
|
438 if not self.include_match.match(filename): |
|
439 return "falls outside the --include trees" |
|
440 else: |
|
441 # We exclude the coverage.py code itself, since a little of it |
|
442 # will be measured otherwise. |
|
443 if self.cover_match.match(filename): |
|
444 return "is part of coverage.py" |
|
445 |
|
446 # If we aren't supposed to trace installed code, then check if this |
|
447 # is near the Python standard library and skip it if so. |
|
448 if self.pylib_match and self.pylib_match.match(filename): |
|
449 return "is in the stdlib" |
|
450 |
|
451 # Exclude anything in the third-party installation areas. |
|
452 if self.third_match.match(filename): |
|
453 return "is a third-party module" |
|
454 |
|
455 # Check the file against the omit pattern. |
|
456 if self.omit_match and self.omit_match.match(filename): |
|
457 return "is inside an --omit pattern" |
|
458 |
|
459 # No point tracing a file we can't later write to SQLite. |
|
460 try: |
|
461 filename.encode("utf-8") |
|
462 except UnicodeEncodeError: |
|
463 return "non-encodable filename" |
|
464 |
|
465 # No reason found to skip this file. |
|
466 return None |
|
467 |
|
468 def warn_conflicting_settings(self): |
|
469 """Warn if there are settings that conflict.""" |
|
470 if self.include: |
|
471 if self.source or self.source_pkgs: |
|
472 self.warn("--include is ignored because --source is set", slug="include-ignored") |
|
473 |
|
474 def warn_already_imported_files(self): |
|
475 """Warn if files have already been imported that we will be measuring.""" |
|
476 if self.include or self.source or self.source_pkgs: |
|
477 warned = set() |
|
478 for mod in list(sys.modules.values()): |
|
479 filename = getattr(mod, "__file__", None) |
|
480 if filename is None: |
|
481 continue |
|
482 if filename in warned: |
|
483 continue |
|
484 |
|
485 if len(getattr(mod, "__path__", ())) > 1: |
|
486 # A namespace package, which confuses this code, so ignore it. |
|
487 continue |
|
488 |
|
489 disp = self.should_trace(filename) |
|
490 if disp.has_dynamic_filename: |
|
491 # A plugin with dynamic filenames: the Python file |
|
492 # shouldn't cause a warning, since it won't be the subject |
|
493 # of tracing anyway. |
|
494 continue |
|
495 if disp.trace: |
|
496 msg = f"Already imported a file that will be measured: {filename}" |
|
497 self.warn(msg, slug="already-imported") |
|
498 warned.add(filename) |
|
499 elif self.debug and self.debug.should('trace'): |
|
500 self.debug.write( |
|
501 "Didn't trace already imported file {!r}: {}".format( |
|
502 disp.original_filename, disp.reason |
|
503 ) |
|
504 ) |
|
505 |
|
506 def warn_unimported_source(self): |
|
507 """Warn about source packages that were of interest, but never traced.""" |
|
508 for pkg in self.source_pkgs_unmatched: |
|
509 self._warn_about_unmeasured_code(pkg) |
|
510 |
|
511 def _warn_about_unmeasured_code(self, pkg): |
|
512 """Warn about a package or module that we never traced. |
|
513 |
|
514 `pkg` is a string, the name of the package or module. |
|
515 |
|
516 """ |
|
517 mod = sys.modules.get(pkg) |
|
518 if mod is None: |
|
519 self.warn(f"Module {pkg} was never imported.", slug="module-not-imported") |
|
520 return |
|
521 |
|
522 if module_is_namespace(mod): |
|
523 # A namespace package. It's OK for this not to have been traced, |
|
524 # since there is no code directly in it. |
|
525 return |
|
526 |
|
527 if not module_has_file(mod): |
|
528 self.warn(f"Module {pkg} has no Python source.", slug="module-not-python") |
|
529 return |
|
530 |
|
531 # The module was in sys.modules, and seems like a module with code, but |
|
532 # we never measured it. I guess that means it was imported before |
|
533 # coverage even started. |
|
534 msg = f"Module {pkg} was previously imported, but not measured" |
|
535 self.warn(msg, slug="module-not-measured") |
|
536 |
|
537 def find_possibly_unexecuted_files(self): |
|
538 """Find files in the areas of interest that might be untraced. |
|
539 |
|
540 Yields pairs: file path, and responsible plug-in name. |
|
541 """ |
|
542 for pkg in self.source_pkgs: |
|
543 if (not pkg in sys.modules or |
|
544 not module_has_file(sys.modules[pkg])): |
|
545 continue |
|
546 pkg_file = source_for_file(sys.modules[pkg].__file__) |
|
547 yield from self._find_executable_files(canonical_path(pkg_file)) |
|
548 |
|
549 for src in self.source: |
|
550 yield from self._find_executable_files(src) |
|
551 |
|
552 def _find_plugin_files(self, src_dir): |
|
553 """Get executable files from the plugins.""" |
|
554 for plugin in self.plugins.file_tracers: |
|
555 for x_file in plugin.find_executable_files(src_dir): |
|
556 yield x_file, plugin._coverage_plugin_name |
|
557 |
|
558 def _find_executable_files(self, src_dir): |
|
559 """Find executable files in `src_dir`. |
|
560 |
|
561 Search for files in `src_dir` that can be executed because they |
|
562 are probably importable. Don't include ones that have been omitted |
|
563 by the configuration. |
|
564 |
|
565 Yield the file path, and the plugin name that handles the file. |
|
566 |
|
567 """ |
|
568 py_files = ((py_file, None) for py_file in find_python_files(src_dir)) |
|
569 plugin_files = self._find_plugin_files(src_dir) |
|
570 |
|
571 for file_path, plugin_name in itertools.chain(py_files, plugin_files): |
|
572 file_path = canonical_filename(file_path) |
|
573 if self.omit_match and self.omit_match.match(file_path): |
|
574 # Turns out this file was omitted, so don't pull it back |
|
575 # in as unexecuted. |
|
576 continue |
|
577 yield file_path, plugin_name |
|
578 |
|
579 def sys_info(self): |
|
580 """Our information for Coverage.sys_info. |
|
581 |
|
582 Returns a list of (key, value) pairs. |
|
583 """ |
|
584 info = [ |
|
585 ("coverage_paths", self.cover_paths), |
|
586 ("stdlib_paths", self.pylib_paths), |
|
587 ("third_party_paths", self.third_paths), |
|
588 ] |
|
589 |
|
590 matcher_names = [ |
|
591 'source_match', 'source_pkgs_match', |
|
592 'include_match', 'omit_match', |
|
593 'cover_match', 'pylib_match', 'third_match', |
|
594 ] |
|
595 |
|
596 for matcher_name in matcher_names: |
|
597 matcher = getattr(self, matcher_name) |
|
598 if matcher: |
|
599 matcher_info = matcher.info() |
|
600 else: |
|
601 matcher_info = '-none-' |
|
602 info.append((matcher_name, matcher_info)) |
|
603 |
|
604 return info |