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