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