src/eric7/DebugClients/Python/coverage/config.py

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

eric ide

mercurial