eric6/DebugClients/Python/coverage/config.py

changeset 6942
2602857055c5
parent 6219
d6c795b5ce33
child 7427
362cd1b6f81a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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

eric ide

mercurial