eric7/DebugClients/Python/coverage/config.py

branch
eric7
changeset 8312
800c432b34c8
parent 7702
f8b97639deb5
child 8527
2bd1325d727e
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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

eric ide

mercurial