1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
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 |
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
3 |
3 |
4 """Determining whether files are being measured/reported or not.""" |
4 """Determining whether files are being measured/reported or not.""" |
5 |
5 |
6 # For finding the stdlib |
6 import importlib.util |
7 import atexit |
|
8 import inspect |
7 import inspect |
9 import itertools |
8 import itertools |
10 import os |
9 import os |
11 import platform |
10 import platform |
12 import re |
11 import re |
13 import sys |
12 import sys |
|
13 import sysconfig |
14 import traceback |
14 import traceback |
15 |
15 |
16 from coverage import env |
16 from coverage import env |
17 from coverage.backward import code_object |
|
18 from coverage.disposition import FileDisposition, disposition_init |
17 from coverage.disposition import FileDisposition, disposition_init |
|
18 from coverage.exceptions import CoverageException |
19 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher |
19 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher |
20 from coverage.files import prep_patterns, find_python_files, canonical_filename |
20 from coverage.files import prep_patterns, find_python_files, canonical_filename |
21 from coverage.misc import CoverageException |
21 from coverage.misc import sys_modules_saved |
22 from coverage.python import source_for_file, source_for_morf |
22 from coverage.python import source_for_file, source_for_morf |
23 |
23 |
24 |
24 |
25 # Pypy has some unusual stuff in the "stdlib". Consider those locations |
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, |
26 # when deciding where the stdlib is. These modules are not used for anything, |
106 if mod__file__ is None: |
106 if mod__file__ is None: |
107 return False |
107 return False |
108 return os.path.exists(mod__file__) |
108 return os.path.exists(mod__file__) |
109 |
109 |
110 |
110 |
111 class InOrOut(object): |
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 ImportError: |
|
124 pass |
|
125 else: |
|
126 if spec is not None: |
|
127 if spec.origin != "namespace": |
|
128 filename = spec.origin |
|
129 path = list(spec.submodule_search_locations or ()) |
|
130 return filename, path |
|
131 |
|
132 |
|
133 def add_stdlib_paths(paths): |
|
134 """Add paths where the stdlib can be found to the set `paths`.""" |
|
135 # Look at where some standard modules are located. That's the |
|
136 # indication for "installed with the interpreter". In some |
|
137 # environments (virtualenv, for example), these modules may be |
|
138 # spread across a few locations. Look at all the candidate modules |
|
139 # we've imported, and take all the different ones. |
|
140 modules_we_happen_to_have = [ |
|
141 inspect, itertools, os, platform, re, sysconfig, traceback, |
|
142 _pypy_irc_topic, _structseq, |
|
143 ] |
|
144 for m in modules_we_happen_to_have: |
|
145 if m is not None and hasattr(m, "__file__"): |
|
146 paths.add(canonical_path(m, directory=True)) |
|
147 |
|
148 if _structseq and not hasattr(_structseq, '__file__'): |
|
149 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
|
150 # objects still have the file names. So dig into one to find |
|
151 # the path to exclude. The "filename" might be synthetic, |
|
152 # don't be fooled by those. |
|
153 structseq_file = _structseq.structseq_new.__code__.co_filename |
|
154 if not structseq_file.startswith("<"): |
|
155 paths.add(canonical_path(structseq_file)) |
|
156 |
|
157 |
|
158 def add_third_party_paths(paths): |
|
159 """Add locations for third-party packages to the set `paths`.""" |
|
160 # Get the paths that sysconfig knows about. |
|
161 scheme_names = set(sysconfig.get_scheme_names()) |
|
162 |
|
163 for scheme in scheme_names: |
|
164 # https://foss.heptapod.net/pypy/pypy/-/issues/3433 |
|
165 better_scheme = "pypy_posix" if scheme == "pypy" else scheme |
|
166 if os.name in better_scheme.split("_"): |
|
167 config_paths = sysconfig.get_paths(scheme) |
|
168 for path_name in ["platlib", "purelib", "scripts"]: |
|
169 paths.add(config_paths[path_name]) |
|
170 |
|
171 |
|
172 def add_coverage_paths(paths): |
|
173 """Add paths where coverage.py code can be found to the set `paths`.""" |
|
174 cover_path = canonical_path(__file__, directory=True) |
|
175 paths.add(cover_path) |
|
176 if env.TESTING: |
|
177 # Don't include our own test code. |
|
178 paths.add(os.path.join(cover_path, "tests")) |
|
179 |
|
180 # When testing, we use PyContracts, which should be considered |
|
181 # part of coverage.py, and it uses six. Exclude those directories |
|
182 # just as we exclude ourselves. |
|
183 if env.USE_CONTRACTS: |
|
184 import contracts |
|
185 import six |
|
186 for mod in [contracts, six]: |
|
187 paths.add(canonical_path(mod)) |
|
188 |
|
189 |
|
190 class InOrOut: |
112 """Machinery for determining what files to measure.""" |
191 """Machinery for determining what files to measure.""" |
113 |
192 |
114 def __init__(self, warn, debug): |
193 def __init__(self, warn, debug): |
115 self.warn = warn |
194 self.warn = warn |
116 self.debug = debug |
195 self.debug = debug |
117 |
196 |
118 # The matchers for should_trace. |
197 # The matchers for should_trace. |
119 self.source_match = None |
198 self.source_match = None |
120 self.source_pkgs_match = None |
199 self.source_pkgs_match = None |
121 self.pylib_paths = self.cover_paths = None |
200 self.pylib_paths = self.cover_paths = self.third_paths = None |
122 self.pylib_match = self.cover_match = None |
201 self.pylib_match = self.cover_match = self.third_match = None |
123 self.include_match = self.omit_match = None |
202 self.include_match = self.omit_match = None |
124 self.plugins = [] |
203 self.plugins = [] |
125 self.disp_class = FileDisposition |
204 self.disp_class = FileDisposition |
126 |
205 |
127 # The source argument can be directories or package names. |
206 # The source argument can be directories or package names. |
128 self.source = [] |
207 self.source = [] |
129 self.source_pkgs = [] |
208 self.source_pkgs = [] |
130 self.source_pkgs_unmatched = [] |
209 self.source_pkgs_unmatched = [] |
131 self.omit = self.include = None |
210 self.omit = self.include = None |
|
211 |
|
212 # Is the source inside a third-party area? |
|
213 self.source_in_third = False |
132 |
214 |
133 def configure(self, config): |
215 def configure(self, config): |
134 """Apply the configuration to get ready for decision-time.""" |
216 """Apply the configuration to get ready for decision-time.""" |
135 self.source_pkgs.extend(config.source_pkgs) |
217 self.source_pkgs.extend(config.source_pkgs) |
136 for src in config.source or []: |
218 for src in config.source or []: |
144 self.include = prep_patterns(config.run_include) |
226 self.include = prep_patterns(config.run_include) |
145 |
227 |
146 # The directories for files considered "installed with the interpreter". |
228 # The directories for files considered "installed with the interpreter". |
147 self.pylib_paths = set() |
229 self.pylib_paths = set() |
148 if not config.cover_pylib: |
230 if not config.cover_pylib: |
149 # Look at where some standard modules are located. That's the |
231 add_stdlib_paths(self.pylib_paths) |
150 # indication for "installed with the interpreter". In some |
|
151 # environments (virtualenv, for example), these modules may be |
|
152 # spread across a few locations. Look at all the candidate modules |
|
153 # we've imported, and take all the different ones. |
|
154 for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): |
|
155 if m is not None and hasattr(m, "__file__"): |
|
156 self.pylib_paths.add(canonical_path(m, directory=True)) |
|
157 |
|
158 if _structseq and not hasattr(_structseq, '__file__'): |
|
159 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
|
160 # objects still have the file names. So dig into one to find |
|
161 # the path to exclude. The "filename" might be synthetic, |
|
162 # don't be fooled by those. |
|
163 structseq_file = code_object(_structseq.structseq_new).co_filename |
|
164 if not structseq_file.startswith("<"): |
|
165 self.pylib_paths.add(canonical_path(structseq_file)) |
|
166 |
232 |
167 # To avoid tracing the coverage.py code itself, we skip anything |
233 # To avoid tracing the coverage.py code itself, we skip anything |
168 # located where we are. |
234 # located where we are. |
169 self.cover_paths = [canonical_path(__file__, directory=True)] |
235 self.cover_paths = set() |
170 if env.TESTING: |
236 add_coverage_paths(self.cover_paths) |
171 # Don't include our own test code. |
237 |
172 self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) |
238 # Find where third-party packages are installed. |
173 |
239 self.third_paths = set() |
174 # When testing, we use PyContracts, which should be considered |
240 add_third_party_paths(self.third_paths) |
175 # part of coverage.py, and it uses six. Exclude those directories |
|
176 # just as we exclude ourselves. |
|
177 import contracts |
|
178 import six |
|
179 for mod in [contracts, six]: |
|
180 self.cover_paths.append(canonical_path(mod)) |
|
181 |
241 |
182 def debug(msg): |
242 def debug(msg): |
183 if self.debug: |
243 if self.debug: |
184 self.debug.write(msg) |
244 self.debug.write(msg) |
185 |
245 |
186 # Create the matchers we need for should_trace |
246 # Create the matchers we need for should_trace |
187 if self.source or self.source_pkgs: |
247 if self.source or self.source_pkgs: |
188 against = [] |
248 against = [] |
189 if self.source: |
249 if self.source: |
190 self.source_match = TreeMatcher(self.source) |
250 self.source_match = TreeMatcher(self.source, "source") |
191 against.append("trees {!r}".format(self.source_match)) |
251 against.append(f"trees {self.source_match!r}") |
192 if self.source_pkgs: |
252 if self.source_pkgs: |
193 self.source_pkgs_match = ModuleMatcher(self.source_pkgs) |
253 self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs") |
194 against.append("modules {!r}".format(self.source_pkgs_match)) |
254 against.append(f"modules {self.source_pkgs_match!r}") |
195 debug("Source matching against " + " and ".join(against)) |
255 debug("Source matching against " + " and ".join(against)) |
196 else: |
256 else: |
197 if self.cover_paths: |
|
198 self.cover_match = TreeMatcher(self.cover_paths) |
|
199 debug("Coverage code matching: {!r}".format(self.cover_match)) |
|
200 if self.pylib_paths: |
257 if self.pylib_paths: |
201 self.pylib_match = TreeMatcher(self.pylib_paths) |
258 self.pylib_match = TreeMatcher(self.pylib_paths, "pylib") |
202 debug("Python stdlib matching: {!r}".format(self.pylib_match)) |
259 debug(f"Python stdlib matching: {self.pylib_match!r}") |
203 if self.include: |
260 if self.include: |
204 self.include_match = FnmatchMatcher(self.include) |
261 self.include_match = FnmatchMatcher(self.include, "include") |
205 debug("Include matching: {!r}".format(self.include_match)) |
262 debug(f"Include matching: {self.include_match!r}") |
206 if self.omit: |
263 if self.omit: |
207 self.omit_match = FnmatchMatcher(self.omit) |
264 self.omit_match = FnmatchMatcher(self.omit, "omit") |
208 debug("Omit matching: {!r}".format(self.omit_match)) |
265 debug(f"Omit matching: {self.omit_match!r}") |
|
266 |
|
267 self.cover_match = TreeMatcher(self.cover_paths, "coverage") |
|
268 debug(f"Coverage code matching: {self.cover_match!r}") |
|
269 |
|
270 self.third_match = TreeMatcher(self.third_paths, "third") |
|
271 debug(f"Third-party lib matching: {self.third_match!r}") |
|
272 |
|
273 # Check if the source we want to measure has been installed as a |
|
274 # third-party package. |
|
275 with sys_modules_saved(): |
|
276 for pkg in self.source_pkgs: |
|
277 try: |
|
278 modfile, path = file_and_path_for_module(pkg) |
|
279 debug(f"Imported source package {pkg!r} as {modfile!r}") |
|
280 except CoverageException as exc: |
|
281 debug(f"Couldn't import source package {pkg!r}: {exc}") |
|
282 continue |
|
283 if modfile: |
|
284 if self.third_match.match(modfile): |
|
285 debug( |
|
286 f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}" |
|
287 ) |
|
288 self.source_in_third = True |
|
289 else: |
|
290 for pathdir in path: |
|
291 if self.third_match.match(pathdir): |
|
292 debug( |
|
293 f"Source is in third-party because of {pkg!r} path directory " + |
|
294 f"at {pathdir!r}" |
|
295 ) |
|
296 self.source_in_third = True |
|
297 |
|
298 for src in self.source: |
|
299 if self.third_match.match(src): |
|
300 debug(f"Source is in third-party because of source directory {src!r}") |
|
301 self.source_in_third = True |
209 |
302 |
210 def should_trace(self, filename, frame=None): |
303 def should_trace(self, filename, frame=None): |
211 """Decide whether to trace execution in `filename`, with a reason. |
304 """Decide whether to trace execution in `filename`, with a reason. |
212 |
305 |
213 This function is called from the trace function. As each new file name |
306 This function is called from the trace function. As each new file name |
255 # file names like "<string>", "<doctest readme.txt[0]>", or |
351 # file names like "<string>", "<doctest readme.txt[0]>", or |
256 # "<exec_function>". Don't ever trace these executions, since we |
352 # "<exec_function>". Don't ever trace these executions, since we |
257 # can't do anything with the data later anyway. |
353 # can't do anything with the data later anyway. |
258 return nope(disp, "not a real file name") |
354 return nope(disp, "not a real file name") |
259 |
355 |
260 # pyexpat does a dumb thing, calling the trace function explicitly from |
|
261 # C code with a C file name. |
|
262 if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): |
|
263 return nope(disp, "pyexpat lies about itself") |
|
264 |
|
265 # Jython reports the .class file to the tracer, use the source file. |
356 # Jython reports the .class file to the tracer, use the source file. |
266 if filename.endswith("$py.class"): |
357 if filename.endswith("$py.class"): |
267 filename = filename[:-9] + ".py" |
358 filename = filename[:-9] + ".py" |
268 |
359 |
269 canonical = canonical_filename(filename) |
360 canonical = canonical_filename(filename) |
287 disp.source_filename = canonical_filename( |
378 disp.source_filename = canonical_filename( |
288 file_tracer.source_filename() |
379 file_tracer.source_filename() |
289 ) |
380 ) |
290 break |
381 break |
291 except Exception: |
382 except Exception: |
292 self.warn( |
383 plugin_name = plugin._coverage_plugin_name |
293 "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name) |
384 tb = traceback.format_exc() |
294 ) |
385 self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}") |
295 traceback.print_exc() |
|
296 plugin._coverage_enabled = False |
386 plugin._coverage_enabled = False |
297 continue |
387 continue |
298 else: |
388 else: |
299 # No plugin wanted it: it's Python. |
389 # No plugin wanted it: it's Python. |
300 disp.trace = True |
390 disp.trace = True |
301 disp.source_filename = canonical |
391 disp.source_filename = canonical |
302 |
392 |
303 if not disp.has_dynamic_filename: |
393 if not disp.has_dynamic_filename: |
304 if not disp.source_filename: |
394 if not disp.source_filename: |
305 raise CoverageException( |
395 raise CoverageException( |
306 "Plugin %r didn't set source_filename for %r" % |
396 f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'" |
307 (plugin, disp.original_filename) |
|
308 ) |
397 ) |
309 reason = self.check_include_omit_etc(disp.source_filename, frame) |
398 reason = self.check_include_omit_etc(disp.source_filename, frame) |
310 if reason: |
399 if reason: |
311 nope(disp, reason) |
400 nope(disp, reason) |
312 |
401 |
332 if self.source_pkgs_match.match(modulename): |
421 if self.source_pkgs_match.match(modulename): |
333 ok = True |
422 ok = True |
334 if modulename in self.source_pkgs_unmatched: |
423 if modulename in self.source_pkgs_unmatched: |
335 self.source_pkgs_unmatched.remove(modulename) |
424 self.source_pkgs_unmatched.remove(modulename) |
336 else: |
425 else: |
337 extra = "module {!r} ".format(modulename) |
426 extra = f"module {modulename!r} " |
338 if not ok and self.source_match: |
427 if not ok and self.source_match: |
339 if self.source_match.match(filename): |
428 if self.source_match.match(filename): |
340 ok = True |
429 ok = True |
341 if not ok: |
430 if not ok: |
342 return extra + "falls outside the --source spec" |
431 return extra + "falls outside the --source spec" |
|
432 if not self.source_in_third: |
|
433 if self.third_match.match(filename): |
|
434 return "inside --source, but is third-party" |
343 elif self.include_match: |
435 elif self.include_match: |
344 if not self.include_match.match(filename): |
436 if not self.include_match.match(filename): |
345 return "falls outside the --include trees" |
437 return "falls outside the --include trees" |
346 else: |
438 else: |
|
439 # We exclude the coverage.py code itself, since a little of it |
|
440 # will be measured otherwise. |
|
441 if self.cover_match.match(filename): |
|
442 return "is part of coverage.py" |
|
443 |
347 # If we aren't supposed to trace installed code, then check if this |
444 # If we aren't supposed to trace installed code, then check if this |
348 # is near the Python standard library and skip it if so. |
445 # is near the Python standard library and skip it if so. |
349 if self.pylib_match and self.pylib_match.match(filename): |
446 if self.pylib_match and self.pylib_match.match(filename): |
350 return "is in the stdlib" |
447 return "is in the stdlib" |
351 |
448 |
352 # We exclude the coverage.py code itself, since a little of it |
449 # Exclude anything in the third-party installation areas. |
353 # will be measured otherwise. |
450 if self.third_match.match(filename): |
354 if self.cover_match and self.cover_match.match(filename): |
451 return "is a third-party module" |
355 return "is part of coverage.py" |
|
356 |
452 |
357 # Check the file against the omit pattern. |
453 # Check the file against the omit pattern. |
358 if self.omit_match and self.omit_match.match(filename): |
454 if self.omit_match and self.omit_match.match(filename): |
359 return "is inside an --omit pattern" |
455 return "is inside an --omit pattern" |
360 |
456 |
361 # No point tracing a file we can't later write to SQLite. |
457 # No point tracing a file we can't later write to SQLite. |
362 try: |
458 try: |
363 filename.encode("utf8") |
459 filename.encode("utf-8") |
364 except UnicodeEncodeError: |
460 except UnicodeEncodeError: |
365 return "non-encodable filename" |
461 return "non-encodable filename" |
366 |
462 |
367 # No reason found to skip this file. |
463 # No reason found to skip this file. |
368 return None |
464 return None |
382 if filename is None: |
478 if filename is None: |
383 continue |
479 continue |
384 if filename in warned: |
480 if filename in warned: |
385 continue |
481 continue |
386 |
482 |
|
483 if len(getattr(mod, "__path__", ())) > 1: |
|
484 # A namespace package, which confuses this code, so ignore it. |
|
485 continue |
|
486 |
387 disp = self.should_trace(filename) |
487 disp = self.should_trace(filename) |
|
488 if disp.has_dynamic_filename: |
|
489 # A plugin with dynamic filenames: the Python file |
|
490 # shouldn't cause a warning, since it won't be the subject |
|
491 # of tracing anyway. |
|
492 continue |
388 if disp.trace: |
493 if disp.trace: |
389 msg = "Already imported a file that will be measured: {}".format(filename) |
494 msg = f"Already imported a file that will be measured: {filename}" |
390 self.warn(msg, slug="already-imported") |
495 self.warn(msg, slug="already-imported") |
391 warned.add(filename) |
496 warned.add(filename) |
|
497 elif self.debug and self.debug.should('trace'): |
|
498 self.debug.write( |
|
499 "Didn't trace already imported file {!r}: {}".format( |
|
500 disp.original_filename, disp.reason |
|
501 ) |
|
502 ) |
392 |
503 |
393 def warn_unimported_source(self): |
504 def warn_unimported_source(self): |
394 """Warn about source packages that were of interest, but never traced.""" |
505 """Warn about source packages that were of interest, but never traced.""" |
395 for pkg in self.source_pkgs_unmatched: |
506 for pkg in self.source_pkgs_unmatched: |
396 self._warn_about_unmeasured_code(pkg) |
507 self._warn_about_unmeasured_code(pkg) |
401 `pkg` is a string, the name of the package or module. |
512 `pkg` is a string, the name of the package or module. |
402 |
513 |
403 """ |
514 """ |
404 mod = sys.modules.get(pkg) |
515 mod = sys.modules.get(pkg) |
405 if mod is None: |
516 if mod is None: |
406 self.warn("Module %s was never imported." % pkg, slug="module-not-imported") |
517 self.warn(f"Module {pkg} was never imported.", slug="module-not-imported") |
407 return |
518 return |
408 |
519 |
409 if module_is_namespace(mod): |
520 if module_is_namespace(mod): |
410 # A namespace package. It's OK for this not to have been traced, |
521 # A namespace package. It's OK for this not to have been traced, |
411 # since there is no code directly in it. |
522 # since there is no code directly in it. |
412 return |
523 return |
413 |
524 |
414 if not module_has_file(mod): |
525 if not module_has_file(mod): |
415 self.warn("Module %s has no Python source." % pkg, slug="module-not-python") |
526 self.warn(f"Module {pkg} has no Python source.", slug="module-not-python") |
416 return |
527 return |
417 |
528 |
418 # The module was in sys.modules, and seems like a module with code, but |
529 # The module was in sys.modules, and seems like a module with code, but |
419 # we never measured it. I guess that means it was imported before |
530 # we never measured it. I guess that means it was imported before |
420 # coverage even started. |
531 # coverage even started. |
421 self.warn( |
532 msg = f"Module {pkg} was previously imported, but not measured" |
422 "Module %s was previously imported, but not measured" % pkg, |
533 self.warn(msg, slug="module-not-measured") |
423 slug="module-not-measured", |
|
424 ) |
|
425 |
534 |
426 def find_possibly_unexecuted_files(self): |
535 def find_possibly_unexecuted_files(self): |
427 """Find files in the areas of interest that might be untraced. |
536 """Find files in the areas of interest that might be untraced. |
428 |
537 |
429 Yields pairs: file path, and responsible plug-in name. |
538 Yields pairs: file path, and responsible plug-in name. |
471 """Our information for Coverage.sys_info. |
578 """Our information for Coverage.sys_info. |
472 |
579 |
473 Returns a list of (key, value) pairs. |
580 Returns a list of (key, value) pairs. |
474 """ |
581 """ |
475 info = [ |
582 info = [ |
476 ('cover_paths', self.cover_paths), |
583 ("coverage_paths", self.cover_paths), |
477 ('pylib_paths', self.pylib_paths), |
584 ("stdlib_paths", self.pylib_paths), |
|
585 ("third_party_paths", self.third_paths), |
478 ] |
586 ] |
479 |
587 |
480 matcher_names = [ |
588 matcher_names = [ |
481 'source_match', 'source_pkgs_match', |
589 'source_match', 'source_pkgs_match', |
482 'include_match', 'omit_match', |
590 'include_match', 'omit_match', |
483 'cover_match', 'pylib_match', |
591 'cover_match', 'pylib_match', 'third_match', |
484 ] |
592 ] |
485 |
593 |
486 for matcher_name in matcher_names: |
594 for matcher_name in matcher_names: |
487 matcher = getattr(self, matcher_name) |
595 matcher = getattr(self, matcher_name) |
488 if matcher: |
596 if matcher: |