7 import os |
7 import os |
8 import re |
8 import re |
9 import sys |
9 import sys |
10 |
10 |
11 from coverage.backward import configparser, iitems, string_class |
11 from coverage.backward import configparser, iitems, string_class |
12 from coverage.misc import CoverageException, isolate_module |
12 from coverage.misc import contract, CoverageException, isolate_module |
13 |
13 |
14 os = isolate_module(os) |
14 os = isolate_module(os) |
15 |
15 |
16 |
16 |
17 class HandyConfigParser(configparser.RawConfigParser): |
17 class HandyConfigParser(configparser.RawConfigParser): |
18 """Our specialization of ConfigParser.""" |
18 """Our specialization of ConfigParser.""" |
19 |
19 |
20 def __init__(self, section_prefix): |
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 |
21 configparser.RawConfigParser.__init__(self) |
28 configparser.RawConfigParser.__init__(self) |
22 self.section_prefix = section_prefix |
29 self.section_prefixes = ["coverage:"] |
23 |
30 if our_file: |
24 def read(self, filename): |
31 self.section_prefixes.append("") |
|
32 |
|
33 def read(self, filenames): |
25 """Read a file name as UTF-8 configuration data.""" |
34 """Read a file name as UTF-8 configuration data.""" |
26 kwargs = {} |
35 kwargs = {} |
27 if sys.version_info >= (3, 2): |
36 if sys.version_info >= (3, 2): |
28 kwargs['encoding'] = "utf-8" |
37 kwargs['encoding'] = "utf-8" |
29 return configparser.RawConfigParser.read(self, filename, **kwargs) |
38 return configparser.RawConfigParser.read(self, filenames, **kwargs) |
30 |
39 |
31 def has_option(self, section, option): |
40 def has_option(self, section, option): |
32 section = self.section_prefix + section |
41 for section_prefix in self.section_prefixes: |
33 return configparser.RawConfigParser.has_option(self, section, option) |
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 |
34 |
47 |
35 def has_section(self, section): |
48 def has_section(self, section): |
36 section = self.section_prefix + section |
49 for section_prefix in self.section_prefixes: |
37 return configparser.RawConfigParser.has_section(self, section) |
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 |
38 |
55 |
39 def options(self, section): |
56 def options(self, section): |
40 section = self.section_prefix + section |
57 for section_prefix in self.section_prefixes: |
41 return configparser.RawConfigParser.options(self, section) |
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 |
42 |
62 |
43 def get_section(self, section): |
63 def get_section(self, section): |
44 """Get the contents of a section, as a dictionary.""" |
64 """Get the contents of a section, as a dictionary.""" |
45 d = {} |
65 d = {} |
46 for opt in self.options(section): |
66 for opt in self.options(section): |
47 d[opt] = self.get(section, opt) |
67 d[opt] = self.get(section, opt) |
48 return d |
68 return d |
49 |
69 |
50 def get(self, section, *args, **kwargs): |
70 def get(self, section, option, *args, **kwargs): # pylint: disable=arguments-differ |
51 """Get a value, replacing environment variables also. |
71 """Get a value, replacing environment variables also. |
52 |
72 |
53 The arguments are the same as `RawConfigParser.get`, but in the found |
73 The arguments are the same as `RawConfigParser.get`, but in the found |
54 value, ``$WORD`` or ``${WORD}`` are replaced by the value of the |
74 value, ``$WORD`` or ``${WORD}`` are replaced by the value of the |
55 environment variable ``WORD``. |
75 environment variable ``WORD``. |
56 |
76 |
57 Returns the finished value. |
77 Returns the finished value. |
58 |
78 |
59 """ |
79 """ |
60 section = self.section_prefix + section |
80 for section_prefix in self.section_prefixes: |
61 v = configparser.RawConfigParser.get(self, section, *args, **kwargs) |
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) |
62 def dollar_replace(m): |
88 def dollar_replace(m): |
63 """Called for each $replacement.""" |
89 """Called for each $replacement.""" |
64 # Only one of the groups will have matched, just get its text. |
90 # Only one of the groups will have matched, just get its text. |
65 word = next(w for w in m.groups() if w is not None) # pragma: part covered |
91 word = next(w for w in m.groups() if w is not None) # pragma: part covered |
66 if word == "$": |
92 if word == "$": |
150 """Initialize the configuration attributes to their defaults.""" |
176 """Initialize the configuration attributes to their defaults.""" |
151 # Metadata about the config. |
177 # Metadata about the config. |
152 self.attempted_config_files = [] |
178 self.attempted_config_files = [] |
153 self.config_files = [] |
179 self.config_files = [] |
154 |
180 |
|
181 # Defaults for [run] and [report] |
|
182 self._include = None |
|
183 self._omit = None |
|
184 |
155 # Defaults for [run] |
185 # Defaults for [run] |
156 self.branch = False |
186 self.branch = False |
157 self.concurrency = None |
187 self.concurrency = None |
158 self.cover_pylib = False |
188 self.cover_pylib = False |
159 self.data_file = ".coverage" |
189 self.data_file = ".coverage" |
160 self.debug = [] |
190 self.debug = [] |
|
191 self.disable_warnings = [] |
161 self.note = None |
192 self.note = None |
162 self.parallel = False |
193 self.parallel = False |
163 self.plugins = [] |
194 self.plugins = [] |
164 self.source = None |
195 self.source = None |
|
196 self.run_include = None |
|
197 self.run_omit = None |
165 self.timid = False |
198 self.timid = False |
166 |
199 |
167 # Defaults for [report] |
200 # Defaults for [report] |
168 self.exclude_list = DEFAULT_EXCLUDE[:] |
201 self.exclude_list = DEFAULT_EXCLUDE[:] |
169 self.fail_under = 0 |
202 self.fail_under = 0.0 |
170 self.ignore_errors = False |
203 self.ignore_errors = False |
171 self.include = None |
204 self.report_include = None |
172 self.omit = None |
205 self.report_omit = None |
173 self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] |
206 self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] |
174 self.partial_list = DEFAULT_PARTIAL[:] |
207 self.partial_list = DEFAULT_PARTIAL[:] |
175 self.precision = 0 |
208 self.precision = 0 |
176 self.show_missing = False |
209 self.show_missing = False |
177 self.skip_covered = False |
210 self.skip_covered = False |
189 self.paths = {} |
222 self.paths = {} |
190 |
223 |
191 # Options for plugins |
224 # Options for plugins |
192 self.plugin_options = {} |
225 self.plugin_options = {} |
193 |
226 |
194 MUST_BE_LIST = ["omit", "include", "debug", "plugins"] |
227 MUST_BE_LIST = [ |
|
228 "debug", "concurrency", "plugins", |
|
229 "report_omit", "report_include", |
|
230 "run_omit", "run_include", |
|
231 ] |
195 |
232 |
196 def from_args(self, **kwargs): |
233 def from_args(self, **kwargs): |
197 """Read config values from `kwargs`.""" |
234 """Read config values from `kwargs`.""" |
198 for k, v in iitems(kwargs): |
235 for k, v in iitems(kwargs): |
199 if v is not None: |
236 if v is not None: |
200 if k in self.MUST_BE_LIST and isinstance(v, string_class): |
237 if k in self.MUST_BE_LIST and isinstance(v, string_class): |
201 v = [v] |
238 v = [v] |
202 setattr(self, k, v) |
239 setattr(self, k, v) |
203 |
240 |
204 def from_file(self, filename, section_prefix=""): |
241 @contract(filename=str) |
|
242 def from_file(self, filename, our_file): |
205 """Read configuration from a .rc file. |
243 """Read configuration from a .rc file. |
206 |
244 |
207 `filename` is a file name to read. |
245 `filename` is a file name to read. |
208 |
246 |
209 Returns True or False, whether the file could be read. |
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. |
210 |
253 |
211 """ |
254 """ |
212 self.attempted_config_files.append(filename) |
255 self.attempted_config_files.append(filename) |
213 |
256 |
214 cp = HandyConfigParser(section_prefix) |
257 cp = HandyConfigParser(our_file) |
215 try: |
258 try: |
216 files_read = cp.read(filename) |
259 files_read = cp.read(filename) |
217 except configparser.Error as err: |
260 except configparser.Error as err: |
218 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) |
261 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) |
219 if not files_read: |
262 if not files_read: |
220 return False |
263 return False |
221 |
264 |
222 self.config_files.extend(files_read) |
265 self.config_files.extend(files_read) |
223 |
266 |
|
267 any_set = False |
224 try: |
268 try: |
225 for option_spec in self.CONFIG_FILE_OPTIONS: |
269 for option_spec in self.CONFIG_FILE_OPTIONS: |
226 self._set_attr_from_config_option(cp, *option_spec) |
270 was_set = self._set_attr_from_config_option(cp, *option_spec) |
|
271 if was_set: |
|
272 any_set = True |
227 except ValueError as err: |
273 except ValueError as err: |
228 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) |
274 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) |
229 |
275 |
230 # Check that there are no unrecognized options. |
276 # Check that there are no unrecognized options. |
231 all_options = collections.defaultdict(set) |
277 all_options = collections.defaultdict(set) |
232 for option_spec in self.CONFIG_FILE_OPTIONS: |
278 for option_spec in self.CONFIG_FILE_OPTIONS: |
233 section, option = option_spec[1].split(":") |
279 section, option = option_spec[1].split(":") |
234 all_options[section].add(option) |
280 all_options[section].add(option) |
235 |
281 |
236 for section, options in iitems(all_options): |
282 for section, options in iitems(all_options): |
237 if cp.has_section(section): |
283 real_section = cp.has_section(section) |
|
284 if real_section: |
238 for unknown in set(cp.options(section)) - options: |
285 for unknown in set(cp.options(section)) - options: |
239 if section_prefix: |
|
240 section = section_prefix + section |
|
241 raise CoverageException( |
286 raise CoverageException( |
242 "Unrecognized option '[%s] %s=' in config file %s" % ( |
287 "Unrecognized option '[%s] %s=' in config file %s" % ( |
243 section, unknown, filename |
288 real_section, unknown, filename |
244 ) |
289 ) |
245 ) |
290 ) |
246 |
291 |
247 # [paths] is special |
292 # [paths] is special |
248 if cp.has_section('paths'): |
293 if cp.has_section('paths'): |
249 for option in cp.options('paths'): |
294 for option in cp.options('paths'): |
250 self.paths[option] = cp.getlist('paths', option) |
295 self.paths[option] = cp.getlist('paths', option) |
|
296 any_set = True |
251 |
297 |
252 # plugins can have options |
298 # plugins can have options |
253 for plugin in self.plugins: |
299 for plugin in self.plugins: |
254 if cp.has_section(plugin): |
300 if cp.has_section(plugin): |
255 self.plugin_options[plugin] = cp.get_section(plugin) |
301 self.plugin_options[plugin] = cp.get_section(plugin) |
256 |
302 any_set = True |
257 return 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 |
258 |
311 |
259 CONFIG_FILE_OPTIONS = [ |
312 CONFIG_FILE_OPTIONS = [ |
260 # These are *args for _set_attr_from_config_option: |
313 # These are *args for _set_attr_from_config_option: |
261 # (attr, where, type_="") |
314 # (attr, where, type_="") |
262 # |
315 # |
265 # type_ is the optional type to apply, by using .getTYPE to read the |
318 # type_ is the optional type to apply, by using .getTYPE to read the |
266 # configuration value from the file. |
319 # configuration value from the file. |
267 |
320 |
268 # [run] |
321 # [run] |
269 ('branch', 'run:branch', 'boolean'), |
322 ('branch', 'run:branch', 'boolean'), |
270 ('concurrency', 'run:concurrency'), |
323 ('concurrency', 'run:concurrency', 'list'), |
271 ('cover_pylib', 'run:cover_pylib', 'boolean'), |
324 ('cover_pylib', 'run:cover_pylib', 'boolean'), |
272 ('data_file', 'run:data_file'), |
325 ('data_file', 'run:data_file'), |
273 ('debug', 'run:debug', 'list'), |
326 ('debug', 'run:debug', 'list'), |
274 ('include', 'run:include', 'list'), |
327 ('disable_warnings', 'run:disable_warnings', 'list'), |
275 ('note', 'run:note'), |
328 ('note', 'run:note'), |
276 ('omit', 'run:omit', 'list'), |
|
277 ('parallel', 'run:parallel', 'boolean'), |
329 ('parallel', 'run:parallel', 'boolean'), |
278 ('plugins', 'run:plugins', 'list'), |
330 ('plugins', 'run:plugins', 'list'), |
|
331 ('run_include', 'run:include', 'list'), |
|
332 ('run_omit', 'run:omit', 'list'), |
279 ('source', 'run:source', 'list'), |
333 ('source', 'run:source', 'list'), |
280 ('timid', 'run:timid', 'boolean'), |
334 ('timid', 'run:timid', 'boolean'), |
281 |
335 |
282 # [report] |
336 # [report] |
283 ('exclude_list', 'report:exclude_lines', 'regexlist'), |
337 ('exclude_list', 'report:exclude_lines', 'regexlist'), |
284 ('fail_under', 'report:fail_under', 'int'), |
338 ('fail_under', 'report:fail_under', 'float'), |
285 ('ignore_errors', 'report:ignore_errors', 'boolean'), |
339 ('ignore_errors', 'report:ignore_errors', 'boolean'), |
286 ('include', 'report:include', 'list'), |
|
287 ('omit', 'report:omit', 'list'), |
|
288 ('partial_always_list', 'report:partial_branches_always', 'regexlist'), |
340 ('partial_always_list', 'report:partial_branches_always', 'regexlist'), |
289 ('partial_list', 'report:partial_branches', 'regexlist'), |
341 ('partial_list', 'report:partial_branches', 'regexlist'), |
290 ('precision', 'report:precision', 'int'), |
342 ('precision', 'report:precision', 'int'), |
|
343 ('report_include', 'report:include', 'list'), |
|
344 ('report_omit', 'report:omit', 'list'), |
291 ('show_missing', 'report:show_missing', 'boolean'), |
345 ('show_missing', 'report:show_missing', 'boolean'), |
292 ('skip_covered', 'report:skip_covered', 'boolean'), |
346 ('skip_covered', 'report:skip_covered', 'boolean'), |
|
347 ('sort', 'report:sort'), |
293 |
348 |
294 # [html] |
349 # [html] |
295 ('extra_css', 'html:extra_css'), |
350 ('extra_css', 'html:extra_css'), |
296 ('html_dir', 'html:directory'), |
351 ('html_dir', 'html:directory'), |
297 ('html_title', 'html:title'), |
352 ('html_title', 'html:title'), |
300 ('xml_output', 'xml:output'), |
355 ('xml_output', 'xml:output'), |
301 ('xml_package_depth', 'xml:package_depth', 'int'), |
356 ('xml_package_depth', 'xml:package_depth', 'int'), |
302 ] |
357 ] |
303 |
358 |
304 def _set_attr_from_config_option(self, cp, attr, where, type_=''): |
359 def _set_attr_from_config_option(self, cp, attr, where, type_=''): |
305 """Set an attribute on self if it exists in the ConfigParser.""" |
360 """Set an attribute on self if it exists in the ConfigParser. |
|
361 |
|
362 Returns True if the attribute was set. |
|
363 |
|
364 """ |
306 section, option = where.split(":") |
365 section, option = where.split(":") |
307 if cp.has_option(section, option): |
366 if cp.has_option(section, option): |
308 method = getattr(cp, 'get' + type_) |
367 method = getattr(cp, 'get' + type_) |
309 setattr(self, attr, method(section, option)) |
368 setattr(self, attr, method(section, option)) |
|
369 return True |
|
370 return False |
310 |
371 |
311 def get_plugin_options(self, plugin): |
372 def get_plugin_options(self, plugin): |
312 """Get a dictionary of options for the plugin named `plugin`.""" |
373 """Get a dictionary of options for the plugin named `plugin`.""" |
313 return self.plugin_options.get(plugin, {}) |
374 return self.plugin_options.get(plugin, {}) |
314 |
375 |
361 if key and plugin_name in self.plugins: |
421 if key and plugin_name in self.plugins: |
362 return self.plugin_options.get(plugin_name, {}).get(key) |
422 return self.plugin_options.get(plugin_name, {}).get(key) |
363 |
423 |
364 # If we get here, we didn't find the option. |
424 # If we get here, we didn't find the option. |
365 raise CoverageException("No such option: %r" % option_name) |
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 |