|
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 """Config file for coverage.py""" |
|
5 |
|
6 import collections |
|
7 import configparser |
|
8 import copy |
|
9 import os |
|
10 import os.path |
|
11 import re |
|
12 |
|
13 from coverage.exceptions import ConfigError |
|
14 from coverage.misc import contract, isolate_module, human_sorted_items, substitute_variables |
|
15 |
|
16 from coverage.tomlconfig import TomlConfigParser, TomlDecodeError |
|
17 |
|
18 os = isolate_module(os) |
|
19 |
|
20 |
|
21 class HandyConfigParser(configparser.RawConfigParser): |
|
22 """Our specialization of ConfigParser.""" |
|
23 |
|
24 def __init__(self, our_file): |
|
25 """Create the HandyConfigParser. |
|
26 |
|
27 `our_file` is True if this config file is specifically for coverage, |
|
28 False if we are examining another config file (tox.ini, setup.cfg) |
|
29 for possible settings. |
|
30 """ |
|
31 |
|
32 configparser.RawConfigParser.__init__(self) |
|
33 self.section_prefixes = ["coverage:"] |
|
34 if our_file: |
|
35 self.section_prefixes.append("") |
|
36 |
|
37 def read(self, filenames, encoding_unused=None): |
|
38 """Read a file name as UTF-8 configuration data.""" |
|
39 return configparser.RawConfigParser.read(self, filenames, encoding="utf-8") |
|
40 |
|
41 def has_option(self, section, option): |
|
42 for section_prefix in self.section_prefixes: |
|
43 real_section = section_prefix + section |
|
44 has = configparser.RawConfigParser.has_option(self, real_section, option) |
|
45 if has: |
|
46 return has |
|
47 return False |
|
48 |
|
49 def has_section(self, section): |
|
50 for section_prefix in self.section_prefixes: |
|
51 real_section = section_prefix + section |
|
52 has = configparser.RawConfigParser.has_section(self, real_section) |
|
53 if has: |
|
54 return real_section |
|
55 return False |
|
56 |
|
57 def options(self, section): |
|
58 for section_prefix in self.section_prefixes: |
|
59 real_section = section_prefix + section |
|
60 if configparser.RawConfigParser.has_section(self, real_section): |
|
61 return configparser.RawConfigParser.options(self, real_section) |
|
62 raise ConfigError(f"No section: {section!r}") |
|
63 |
|
64 def get_section(self, section): |
|
65 """Get the contents of a section, as a dictionary.""" |
|
66 d = {} |
|
67 for opt in self.options(section): |
|
68 d[opt] = self.get(section, opt) |
|
69 return d |
|
70 |
|
71 def get(self, section, option, *args, **kwargs): |
|
72 """Get a value, replacing environment variables also. |
|
73 |
|
74 The arguments are the same as `RawConfigParser.get`, but in the found |
|
75 value, ``$WORD`` or ``${WORD}`` are replaced by the value of the |
|
76 environment variable ``WORD``. |
|
77 |
|
78 Returns the finished value. |
|
79 |
|
80 """ |
|
81 for section_prefix in self.section_prefixes: |
|
82 real_section = section_prefix + section |
|
83 if configparser.RawConfigParser.has_option(self, real_section, option): |
|
84 break |
|
85 else: |
|
86 raise ConfigError(f"No option {option!r} in section: {section!r}") |
|
87 |
|
88 v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) |
|
89 v = substitute_variables(v, os.environ) |
|
90 return v |
|
91 |
|
92 def getlist(self, section, option): |
|
93 """Read a list of strings. |
|
94 |
|
95 The value of `section` and `option` is treated as a comma- and newline- |
|
96 separated list of strings. Each value is stripped of whitespace. |
|
97 |
|
98 Returns the list of strings. |
|
99 |
|
100 """ |
|
101 value_list = self.get(section, option) |
|
102 values = [] |
|
103 for value_line in value_list.split('\n'): |
|
104 for value in value_line.split(','): |
|
105 value = value.strip() |
|
106 if value: |
|
107 values.append(value) |
|
108 return values |
|
109 |
|
110 def getregexlist(self, section, option): |
|
111 """Read a list of full-line regexes. |
|
112 |
|
113 The value of `section` and `option` is treated as a newline-separated |
|
114 list of regexes. Each value is stripped of whitespace. |
|
115 |
|
116 Returns the list of strings. |
|
117 |
|
118 """ |
|
119 line_list = self.get(section, option) |
|
120 value_list = [] |
|
121 for value in line_list.splitlines(): |
|
122 value = value.strip() |
|
123 try: |
|
124 re.compile(value) |
|
125 except re.error as e: |
|
126 raise ConfigError( |
|
127 f"Invalid [{section}].{option} value {value!r}: {e}" |
|
128 ) from e |
|
129 if value: |
|
130 value_list.append(value) |
|
131 return value_list |
|
132 |
|
133 |
|
134 # The default line exclusion regexes. |
|
135 DEFAULT_EXCLUDE = [ |
|
136 r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)', |
|
137 ] |
|
138 |
|
139 # The default partial branch regexes, to be modified by the user. |
|
140 DEFAULT_PARTIAL = [ |
|
141 r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)', |
|
142 ] |
|
143 |
|
144 # The default partial branch regexes, based on Python semantics. |
|
145 # These are any Python branching constructs that can't actually execute all |
|
146 # their branches. |
|
147 DEFAULT_PARTIAL_ALWAYS = [ |
|
148 'while (True|1|False|0):', |
|
149 'if (True|1|False|0):', |
|
150 ] |
|
151 |
|
152 |
|
153 class CoverageConfig: |
|
154 """Coverage.py configuration. |
|
155 |
|
156 The attributes of this class are the various settings that control the |
|
157 operation of coverage.py. |
|
158 |
|
159 """ |
|
160 # pylint: disable=too-many-instance-attributes |
|
161 |
|
162 def __init__(self): |
|
163 """Initialize the configuration attributes to their defaults.""" |
|
164 # Metadata about the config. |
|
165 # We tried to read these config files. |
|
166 self.attempted_config_files = [] |
|
167 # We did read these config files, but maybe didn't find any content for us. |
|
168 self.config_files_read = [] |
|
169 # The file that gave us our configuration. |
|
170 self.config_file = None |
|
171 self._config_contents = None |
|
172 |
|
173 # Defaults for [run] and [report] |
|
174 self._include = None |
|
175 self._omit = None |
|
176 |
|
177 # Defaults for [run] |
|
178 self.branch = False |
|
179 self.command_line = None |
|
180 self.concurrency = None |
|
181 self.context = None |
|
182 self.cover_pylib = False |
|
183 self.data_file = ".coverage" |
|
184 self.debug = [] |
|
185 self.disable_warnings = [] |
|
186 self.dynamic_context = None |
|
187 self.note = None |
|
188 self.parallel = False |
|
189 self.plugins = [] |
|
190 self.relative_files = False |
|
191 self.run_include = None |
|
192 self.run_omit = None |
|
193 self.sigterm = False |
|
194 self.source = None |
|
195 self.source_pkgs = [] |
|
196 self.timid = False |
|
197 self._crash = None |
|
198 |
|
199 # Defaults for [report] |
|
200 self.exclude_list = DEFAULT_EXCLUDE[:] |
|
201 self.fail_under = 0.0 |
|
202 self.ignore_errors = False |
|
203 self.report_include = None |
|
204 self.report_omit = None |
|
205 self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] |
|
206 self.partial_list = DEFAULT_PARTIAL[:] |
|
207 self.precision = 0 |
|
208 self.report_contexts = None |
|
209 self.show_missing = False |
|
210 self.skip_covered = False |
|
211 self.skip_empty = False |
|
212 self.sort = None |
|
213 |
|
214 # Defaults for [html] |
|
215 self.extra_css = None |
|
216 self.html_dir = "htmlcov" |
|
217 self.html_skip_covered = None |
|
218 self.html_skip_empty = None |
|
219 self.html_title = "Coverage report" |
|
220 self.show_contexts = False |
|
221 |
|
222 # Defaults for [xml] |
|
223 self.xml_output = "coverage.xml" |
|
224 self.xml_package_depth = 99 |
|
225 |
|
226 # Defaults for [json] |
|
227 self.json_output = "coverage.json" |
|
228 self.json_pretty_print = False |
|
229 self.json_show_contexts = False |
|
230 |
|
231 # Defaults for [lcov] |
|
232 self.lcov_output = "coverage.lcov" |
|
233 |
|
234 # Defaults for [paths] |
|
235 self.paths = collections.OrderedDict() |
|
236 |
|
237 # Options for plugins |
|
238 self.plugin_options = {} |
|
239 |
|
240 MUST_BE_LIST = { |
|
241 "debug", "concurrency", "plugins", |
|
242 "report_omit", "report_include", |
|
243 "run_omit", "run_include", |
|
244 } |
|
245 |
|
246 def from_args(self, **kwargs): |
|
247 """Read config values from `kwargs`.""" |
|
248 for k, v in kwargs.items(): |
|
249 if v is not None: |
|
250 if k in self.MUST_BE_LIST and isinstance(v, str): |
|
251 v = [v] |
|
252 setattr(self, k, v) |
|
253 |
|
254 @contract(filename=str) |
|
255 def from_file(self, filename, warn, our_file): |
|
256 """Read configuration from a .rc file. |
|
257 |
|
258 `filename` is a file name to read. |
|
259 |
|
260 `our_file` is True if this config file is specifically for coverage, |
|
261 False if we are examining another config file (tox.ini, setup.cfg) |
|
262 for possible settings. |
|
263 |
|
264 Returns True or False, whether the file could be read, and it had some |
|
265 coverage.py settings in it. |
|
266 |
|
267 """ |
|
268 _, ext = os.path.splitext(filename) |
|
269 if ext == '.toml': |
|
270 cp = TomlConfigParser(our_file) |
|
271 else: |
|
272 cp = HandyConfigParser(our_file) |
|
273 |
|
274 self.attempted_config_files.append(filename) |
|
275 |
|
276 try: |
|
277 files_read = cp.read(filename) |
|
278 except (configparser.Error, TomlDecodeError) as err: |
|
279 raise ConfigError(f"Couldn't read config file {filename}: {err}") from err |
|
280 if not files_read: |
|
281 return False |
|
282 |
|
283 self.config_files_read.extend(map(os.path.abspath, files_read)) |
|
284 |
|
285 any_set = False |
|
286 try: |
|
287 for option_spec in self.CONFIG_FILE_OPTIONS: |
|
288 was_set = self._set_attr_from_config_option(cp, *option_spec) |
|
289 if was_set: |
|
290 any_set = True |
|
291 except ValueError as err: |
|
292 raise ConfigError(f"Couldn't read config file {filename}: {err}") from err |
|
293 |
|
294 # Check that there are no unrecognized options. |
|
295 all_options = collections.defaultdict(set) |
|
296 for option_spec in self.CONFIG_FILE_OPTIONS: |
|
297 section, option = option_spec[1].split(":") |
|
298 all_options[section].add(option) |
|
299 |
|
300 for section, options in all_options.items(): |
|
301 real_section = cp.has_section(section) |
|
302 if real_section: |
|
303 for unknown in set(cp.options(section)) - options: |
|
304 warn( |
|
305 "Unrecognized option '[{}] {}=' in config file {}".format( |
|
306 real_section, unknown, filename |
|
307 ) |
|
308 ) |
|
309 |
|
310 # [paths] is special |
|
311 if cp.has_section('paths'): |
|
312 for option in cp.options('paths'): |
|
313 self.paths[option] = cp.getlist('paths', option) |
|
314 any_set = True |
|
315 |
|
316 # plugins can have options |
|
317 for plugin in self.plugins: |
|
318 if cp.has_section(plugin): |
|
319 self.plugin_options[plugin] = cp.get_section(plugin) |
|
320 any_set = True |
|
321 |
|
322 # Was this file used as a config file? If it's specifically our file, |
|
323 # then it was used. If we're piggybacking on someone else's file, |
|
324 # then it was only used if we found some settings in it. |
|
325 if our_file: |
|
326 used = True |
|
327 else: |
|
328 used = any_set |
|
329 |
|
330 if used: |
|
331 self.config_file = os.path.abspath(filename) |
|
332 with open(filename, "rb") as f: |
|
333 self._config_contents = f.read() |
|
334 |
|
335 return used |
|
336 |
|
337 def copy(self): |
|
338 """Return a copy of the configuration.""" |
|
339 return copy.deepcopy(self) |
|
340 |
|
341 CONCURRENCY_CHOICES = {"thread", "gevent", "greenlet", "eventlet", "multiprocessing"} |
|
342 |
|
343 CONFIG_FILE_OPTIONS = [ |
|
344 # These are *args for _set_attr_from_config_option: |
|
345 # (attr, where, type_="") |
|
346 # |
|
347 # attr is the attribute to set on the CoverageConfig object. |
|
348 # where is the section:name to read from the configuration file. |
|
349 # type_ is the optional type to apply, by using .getTYPE to read the |
|
350 # configuration value from the file. |
|
351 |
|
352 # [run] |
|
353 ('branch', 'run:branch', 'boolean'), |
|
354 ('command_line', 'run:command_line'), |
|
355 ('concurrency', 'run:concurrency', 'list'), |
|
356 ('context', 'run:context'), |
|
357 ('cover_pylib', 'run:cover_pylib', 'boolean'), |
|
358 ('data_file', 'run:data_file'), |
|
359 ('debug', 'run:debug', 'list'), |
|
360 ('disable_warnings', 'run:disable_warnings', 'list'), |
|
361 ('dynamic_context', 'run:dynamic_context'), |
|
362 ('note', 'run:note'), |
|
363 ('parallel', 'run:parallel', 'boolean'), |
|
364 ('plugins', 'run:plugins', 'list'), |
|
365 ('relative_files', 'run:relative_files', 'boolean'), |
|
366 ('run_include', 'run:include', 'list'), |
|
367 ('run_omit', 'run:omit', 'list'), |
|
368 ('sigterm', 'run:sigterm', 'boolean'), |
|
369 ('source', 'run:source', 'list'), |
|
370 ('source_pkgs', 'run:source_pkgs', 'list'), |
|
371 ('timid', 'run:timid', 'boolean'), |
|
372 ('_crash', 'run:_crash'), |
|
373 |
|
374 # [report] |
|
375 ('exclude_list', 'report:exclude_lines', 'regexlist'), |
|
376 ('fail_under', 'report:fail_under', 'float'), |
|
377 ('ignore_errors', 'report:ignore_errors', 'boolean'), |
|
378 ('partial_always_list', 'report:partial_branches_always', 'regexlist'), |
|
379 ('partial_list', 'report:partial_branches', 'regexlist'), |
|
380 ('precision', 'report:precision', 'int'), |
|
381 ('report_contexts', 'report:contexts', 'list'), |
|
382 ('report_include', 'report:include', 'list'), |
|
383 ('report_omit', 'report:omit', 'list'), |
|
384 ('show_missing', 'report:show_missing', 'boolean'), |
|
385 ('skip_covered', 'report:skip_covered', 'boolean'), |
|
386 ('skip_empty', 'report:skip_empty', 'boolean'), |
|
387 ('sort', 'report:sort'), |
|
388 |
|
389 # [html] |
|
390 ('extra_css', 'html:extra_css'), |
|
391 ('html_dir', 'html:directory'), |
|
392 ('html_skip_covered', 'html:skip_covered', 'boolean'), |
|
393 ('html_skip_empty', 'html:skip_empty', 'boolean'), |
|
394 ('html_title', 'html:title'), |
|
395 ('show_contexts', 'html:show_contexts', 'boolean'), |
|
396 |
|
397 # [xml] |
|
398 ('xml_output', 'xml:output'), |
|
399 ('xml_package_depth', 'xml:package_depth', 'int'), |
|
400 |
|
401 # [json] |
|
402 ('json_output', 'json:output'), |
|
403 ('json_pretty_print', 'json:pretty_print', 'boolean'), |
|
404 ('json_show_contexts', 'json:show_contexts', 'boolean'), |
|
405 |
|
406 # [lcov] |
|
407 ('lcov_output', 'lcov:output'), |
|
408 ] |
|
409 |
|
410 def _set_attr_from_config_option(self, cp, attr, where, type_=''): |
|
411 """Set an attribute on self if it exists in the ConfigParser. |
|
412 |
|
413 Returns True if the attribute was set. |
|
414 |
|
415 """ |
|
416 section, option = where.split(":") |
|
417 if cp.has_option(section, option): |
|
418 method = getattr(cp, 'get' + type_) |
|
419 setattr(self, attr, method(section, option)) |
|
420 return True |
|
421 return False |
|
422 |
|
423 def get_plugin_options(self, plugin): |
|
424 """Get a dictionary of options for the plugin named `plugin`.""" |
|
425 return self.plugin_options.get(plugin, {}) |
|
426 |
|
427 def set_option(self, option_name, value): |
|
428 """Set an option in the configuration. |
|
429 |
|
430 `option_name` is a colon-separated string indicating the section and |
|
431 option name. For example, the ``branch`` option in the ``[run]`` |
|
432 section of the config file would be indicated with `"run:branch"`. |
|
433 |
|
434 `value` is the new value for the option. |
|
435 |
|
436 """ |
|
437 # Special-cased options. |
|
438 if option_name == "paths": |
|
439 self.paths = value |
|
440 return |
|
441 |
|
442 # Check all the hard-coded options. |
|
443 for option_spec in self.CONFIG_FILE_OPTIONS: |
|
444 attr, where = option_spec[:2] |
|
445 if where == option_name: |
|
446 setattr(self, attr, value) |
|
447 return |
|
448 |
|
449 # See if it's a plugin option. |
|
450 plugin_name, _, key = option_name.partition(":") |
|
451 if key and plugin_name in self.plugins: |
|
452 self.plugin_options.setdefault(plugin_name, {})[key] = value |
|
453 return |
|
454 |
|
455 # If we get here, we didn't find the option. |
|
456 raise ConfigError(f"No such option: {option_name!r}") |
|
457 |
|
458 def get_option(self, option_name): |
|
459 """Get an option from the configuration. |
|
460 |
|
461 `option_name` is a colon-separated string indicating the section and |
|
462 option name. For example, the ``branch`` option in the ``[run]`` |
|
463 section of the config file would be indicated with `"run:branch"`. |
|
464 |
|
465 Returns the value of the option. |
|
466 |
|
467 """ |
|
468 # Special-cased options. |
|
469 if option_name == "paths": |
|
470 return self.paths |
|
471 |
|
472 # Check all the hard-coded options. |
|
473 for option_spec in self.CONFIG_FILE_OPTIONS: |
|
474 attr, where = option_spec[:2] |
|
475 if where == option_name: |
|
476 return getattr(self, attr) |
|
477 |
|
478 # See if it's a plugin option. |
|
479 plugin_name, _, key = option_name.partition(":") |
|
480 if key and plugin_name in self.plugins: |
|
481 return self.plugin_options.get(plugin_name, {}).get(key) |
|
482 |
|
483 # If we get here, we didn't find the option. |
|
484 raise ConfigError(f"No such option: {option_name!r}") |
|
485 |
|
486 def post_process_file(self, path): |
|
487 """Make final adjustments to a file path to make it usable.""" |
|
488 return os.path.expanduser(path) |
|
489 |
|
490 def post_process(self): |
|
491 """Make final adjustments to settings to make them usable.""" |
|
492 self.data_file = self.post_process_file(self.data_file) |
|
493 self.html_dir = self.post_process_file(self.html_dir) |
|
494 self.xml_output = self.post_process_file(self.xml_output) |
|
495 self.paths = collections.OrderedDict( |
|
496 (k, [self.post_process_file(f) for f in v]) |
|
497 for k, v in self.paths.items() |
|
498 ) |
|
499 |
|
500 def debug_info(self): |
|
501 """Make a list of (name, value) pairs for writing debug info.""" |
|
502 return human_sorted_items( |
|
503 (k, v) for k, v in self.__dict__.items() if not k.startswith("_") |
|
504 ) |
|
505 |
|
506 |
|
507 def config_files_to_try(config_file): |
|
508 """What config files should we try to read? |
|
509 |
|
510 Returns a list of tuples: |
|
511 (filename, is_our_file, was_file_specified) |
|
512 """ |
|
513 |
|
514 # Some API users were specifying ".coveragerc" to mean the same as |
|
515 # True, so make it so. |
|
516 if config_file == ".coveragerc": |
|
517 config_file = True |
|
518 specified_file = (config_file is not True) |
|
519 if not specified_file: |
|
520 # No file was specified. Check COVERAGE_RCFILE. |
|
521 config_file = os.environ.get('COVERAGE_RCFILE') |
|
522 if config_file: |
|
523 specified_file = True |
|
524 if not specified_file: |
|
525 # Still no file specified. Default to .coveragerc |
|
526 config_file = ".coveragerc" |
|
527 files_to_try = [ |
|
528 (config_file, True, specified_file), |
|
529 ("setup.cfg", False, False), |
|
530 ("tox.ini", False, False), |
|
531 ("pyproject.toml", False, False), |
|
532 ] |
|
533 return files_to_try |
|
534 |
|
535 |
|
536 def read_coverage_config(config_file, warn, **kwargs): |
|
537 """Read the coverage.py configuration. |
|
538 |
|
539 Arguments: |
|
540 config_file: a boolean or string, see the `Coverage` class for the |
|
541 tricky details. |
|
542 warn: a function to issue warnings. |
|
543 all others: keyword arguments from the `Coverage` class, used for |
|
544 setting values in the configuration. |
|
545 |
|
546 Returns: |
|
547 config: |
|
548 config is a CoverageConfig object read from the appropriate |
|
549 configuration file. |
|
550 |
|
551 """ |
|
552 # Build the configuration from a number of sources: |
|
553 # 1) defaults: |
|
554 config = CoverageConfig() |
|
555 |
|
556 # 2) from a file: |
|
557 if config_file: |
|
558 files_to_try = config_files_to_try(config_file) |
|
559 |
|
560 for fname, our_file, specified_file in files_to_try: |
|
561 config_read = config.from_file(fname, warn, our_file=our_file) |
|
562 if config_read: |
|
563 break |
|
564 if specified_file: |
|
565 raise ConfigError(f"Couldn't read {fname!r} as a config file") |
|
566 |
|
567 # $set_env.py: COVERAGE_DEBUG - Options for --debug. |
|
568 # 3) from environment variables: |
|
569 env_data_file = os.environ.get('COVERAGE_FILE') |
|
570 if env_data_file: |
|
571 config.data_file = env_data_file |
|
572 debugs = os.environ.get('COVERAGE_DEBUG') |
|
573 if debugs: |
|
574 config.debug.extend(d.strip() for d in debugs.split(",")) |
|
575 |
|
576 # 4) from constructor arguments: |
|
577 config.from_args(**kwargs) |
|
578 |
|
579 # Once all the config has been collected, there's a little post-processing |
|
580 # to do. |
|
581 config.post_process() |
|
582 |
|
583 return config |