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 6import collections 7import os 8import re 9import sys 10 11from coverage.backward import configparser, iitems, string_class 12from coverage.misc import CoverageException, isolate_module 13 14os = isolate_module(os) 15 16 17class HandyConfigParser(configparser.RawConfigParser): 18 """Our specialization of ConfigParser.""" 19 20 def __init__(self, section_prefix): 21 configparser.RawConfigParser.__init__(self) 22 self.section_prefix = section_prefix 23 24 def read(self, filename): 25 """Read a file name as UTF-8 configuration data.""" 26 kwargs = {} 27 if sys.version_info >= (3, 2): 28 kwargs['encoding'] = "utf-8" 29 return configparser.RawConfigParser.read(self, filename, **kwargs) 30 31 def has_option(self, section, option): 32 section = self.section_prefix + section 33 return configparser.RawConfigParser.has_option(self, section, option) 34 35 def has_section(self, section): 36 section = self.section_prefix + section 37 return configparser.RawConfigParser.has_section(self, section) 38 39 def options(self, section): 40 section = self.section_prefix + section 41 return configparser.RawConfigParser.options(self, section) 42 43 def get_section(self, section): 44 """Get the contents of a section, as a dictionary.""" 45 d = {} 46 for opt in self.options(section): 47 d[opt] = self.get(section, opt) 48 return d 49 50 def get(self, section, *args, **kwargs): 51 """Get a value, replacing environment variables also. 52 53 The arguments are the same as `RawConfigParser.get`, but in the found 54 value, ``$WORD`` or ``${WORD}`` are replaced by the value of the 55 environment variable ``WORD``. 56 57 Returns the finished value. 58 59 """ 60 section = self.section_prefix + section 61 v = configparser.RawConfigParser.get(self, section, *args, **kwargs) 62 def dollar_replace(m): 63 """Called for each $replacement.""" 64 # 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 66 if word == "$": 67 return "$" 68 else: 69 return os.environ.get(word, '') 70 71 dollar_pattern = r"""(?x) # Use extended regex syntax 72 \$(?: # A dollar sign, then 73 (?P<v1>\w+) | # a plain word, 74 {(?P<v2>\w+)} | # or a {-wrapped word, 75 (?P<char>[$]) # or a dollar sign. 76 ) 77 """ 78 v = re.sub(dollar_pattern, dollar_replace, v) 79 return v 80 81 def getlist(self, section, option): 82 """Read a list of strings. 83 84 The value of `section` and `option` is treated as a comma- and newline- 85 separated list of strings. Each value is stripped of whitespace. 86 87 Returns the list of strings. 88 89 """ 90 value_list = self.get(section, option) 91 values = [] 92 for value_line in value_list.split('\n'): 93 for value in value_line.split(','): 94 value = value.strip() 95 if value: 96 values.append(value) 97 return values 98 99 def getregexlist(self, section, option): 100 """Read a list of full-line regexes. 101 102 The value of `section` and `option` is treated as a newline-separated 103 list of regexes. Each value is stripped of whitespace. 104 105 Returns the list of strings. 106 107 """ 108 line_list = self.get(section, option) 109 value_list = [] 110 for value in line_list.splitlines(): 111 value = value.strip() 112 try: 113 re.compile(value) 114 except re.error as e: 115 raise CoverageException( 116 "Invalid [%s].%s value %r: %s" % (section, option, value, e) 117 ) 118 if value: 119 value_list.append(value) 120 return value_list 121 122 123# The default line exclusion regexes. 124DEFAULT_EXCLUDE = [ 125 r'(?i)#\s*pragma[:\s]?\s*no\s*cover', 126] 127 128# The default partial branch regexes, to be modified by the user. 129DEFAULT_PARTIAL = [ 130 r'(?i)#\s*pragma[:\s]?\s*no\s*branch', 131] 132 133# The default partial branch regexes, based on Python semantics. 134# These are any Python branching constructs that can't actually execute all 135# their branches. 136DEFAULT_PARTIAL_ALWAYS = [ 137 'while (True|1|False|0):', 138 'if (True|1|False|0):', 139] 140 141 142class CoverageConfig(object): 143 """Coverage.py configuration. 144 145 The attributes of this class are the various settings that control the 146 operation of coverage.py. 147 148 """ 149 def __init__(self): 150 """Initialize the configuration attributes to their defaults.""" 151 # Metadata about the config. 152 self.attempted_config_files = [] 153 self.config_files = [] 154 155 # Defaults for [run] 156 self.branch = False 157 self.concurrency = None 158 self.cover_pylib = False 159 self.data_file = ".coverage" 160 self.debug = [] 161 self.note = None 162 self.parallel = False 163 self.plugins = [] 164 self.source = None 165 self.timid = False 166 167 # Defaults for [report] 168 self.exclude_list = DEFAULT_EXCLUDE[:] 169 self.fail_under = 0 170 self.ignore_errors = False 171 self.include = None 172 self.omit = None 173 self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] 174 self.partial_list = DEFAULT_PARTIAL[:] 175 self.precision = 0 176 self.show_missing = False 177 self.skip_covered = False 178 179 # Defaults for [html] 180 self.extra_css = None 181 self.html_dir = "htmlcov" 182 self.html_title = "Coverage report" 183 184 # Defaults for [xml] 185 self.xml_output = "coverage.xml" 186 self.xml_package_depth = 99 187 188 # Defaults for [paths] 189 self.paths = {} 190 191 # Options for plugins 192 self.plugin_options = {} 193 194 MUST_BE_LIST = ["omit", "include", "debug", "plugins"] 195 196 def from_args(self, **kwargs): 197 """Read config values from `kwargs`.""" 198 for k, v in iitems(kwargs): 199 if v is not None: 200 if k in self.MUST_BE_LIST and isinstance(v, string_class): 201 v = [v] 202 setattr(self, k, v) 203 204 def from_file(self, filename, section_prefix=""): 205 """Read configuration from a .rc file. 206 207 `filename` is a file name to read. 208 209 Returns True or False, whether the file could be read. 210 211 """ 212 self.attempted_config_files.append(filename) 213 214 cp = HandyConfigParser(section_prefix) 215 try: 216 files_read = cp.read(filename) 217 except configparser.Error as err: 218 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) 219 if not files_read: 220 return False 221 222 self.config_files.extend(files_read) 223 224 try: 225 for option_spec in self.CONFIG_FILE_OPTIONS: 226 self._set_attr_from_config_option(cp, *option_spec) 227 except ValueError as err: 228 raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) 229 230 # Check that there are no unrecognized options. 231 all_options = collections.defaultdict(set) 232 for option_spec in self.CONFIG_FILE_OPTIONS: 233 section, option = option_spec[1].split(":") 234 all_options[section].add(option) 235 236 for section, options in iitems(all_options): 237 if cp.has_section(section): 238 for unknown in set(cp.options(section)) - options: 239 if section_prefix: 240 section = section_prefix + section 241 raise CoverageException( 242 "Unrecognized option '[%s] %s=' in config file %s" % ( 243 section, unknown, filename 244 ) 245 ) 246 247 # [paths] is special 248 if cp.has_section('paths'): 249 for option in cp.options('paths'): 250 self.paths[option] = cp.getlist('paths', option) 251 252 # plugins can have options 253 for plugin in self.plugins: 254 if cp.has_section(plugin): 255 self.plugin_options[plugin] = cp.get_section(plugin) 256 257 return True 258 259 CONFIG_FILE_OPTIONS = [ 260 # These are *args for _set_attr_from_config_option: 261 # (attr, where, type_="") 262 # 263 # attr is the attribute to set on the CoverageConfig object. 264 # where is the section:name to read from the configuration file. 265 # type_ is the optional type to apply, by using .getTYPE to read the 266 # configuration value from the file. 267 268 # [run] 269 ('branch', 'run:branch', 'boolean'), 270 ('concurrency', 'run:concurrency'), 271 ('cover_pylib', 'run:cover_pylib', 'boolean'), 272 ('data_file', 'run:data_file'), 273 ('debug', 'run:debug', 'list'), 274 ('include', 'run:include', 'list'), 275 ('note', 'run:note'), 276 ('omit', 'run:omit', 'list'), 277 ('parallel', 'run:parallel', 'boolean'), 278 ('plugins', 'run:plugins', 'list'), 279 ('source', 'run:source', 'list'), 280 ('timid', 'run:timid', 'boolean'), 281 282 # [report] 283 ('exclude_list', 'report:exclude_lines', 'regexlist'), 284 ('fail_under', 'report:fail_under', 'int'), 285 ('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'), 289 ('partial_list', 'report:partial_branches', 'regexlist'), 290 ('precision', 'report:precision', 'int'), 291 ('show_missing', 'report:show_missing', 'boolean'), 292 ('skip_covered', 'report:skip_covered', 'boolean'), 293 294 # [html] 295 ('extra_css', 'html:extra_css'), 296 ('html_dir', 'html:directory'), 297 ('html_title', 'html:title'), 298 299 # [xml] 300 ('xml_output', 'xml:output'), 301 ('xml_package_depth', 'xml:package_depth', 'int'), 302 ] 303 304 def _set_attr_from_config_option(self, cp, attr, where, type_=''): 305 """Set an attribute on self if it exists in the ConfigParser.""" 306 section, option = where.split(":") 307 if cp.has_option(section, option): 308 method = getattr(cp, 'get' + type_) 309 setattr(self, attr, method(section, option)) 310 311 def get_plugin_options(self, plugin): 312 """Get a dictionary of options for the plugin named `plugin`.""" 313 return self.plugin_options.get(plugin, {}) 314 315 def set_option(self, option_name, value): 316 """Set an option in the configuration. 317 318 `option_name` is a colon-separated string indicating the section and 319 option name. For example, the ``branch`` option in the ``[run]`` 320 section of the config file would be indicated with `"run:branch"`. 321 322 `value` is the new value for the option. 323 324 """ 325 326 # Check all the hard-coded options. 327 for option_spec in self.CONFIG_FILE_OPTIONS: 328 attr, where = option_spec[:2] 329 if where == option_name: 330 setattr(self, attr, value) 331 return 332 333 # See if it's a plugin option. 334 plugin_name, _, key = option_name.partition(":") 335 if key and plugin_name in self.plugins: 336 self.plugin_options.setdefault(plugin_name, {})[key] = value 337 return 338 339 # If we get here, we didn't find the option. 340 raise CoverageException("No such option: %r" % option_name) 341 342 def get_option(self, option_name): 343 """Get an option from the configuration. 344 345 `option_name` is a colon-separated string indicating the section and 346 option name. For example, the ``branch`` option in the ``[run]`` 347 section of the config file would be indicated with `"run:branch"`. 348 349 Returns the value of the option. 350 351 """ 352 353 # Check all the hard-coded options. 354 for option_spec in self.CONFIG_FILE_OPTIONS: 355 attr, where = option_spec[:2] 356 if where == option_name: 357 return getattr(self, attr) 358 359 # See if it's a plugin option. 360 plugin_name, _, key = option_name.partition(":") 361 if key and plugin_name in self.plugins: 362 return self.plugin_options.get(plugin_name, {}).get(key) 363 364 # If we get here, we didn't find the option. 365 raise CoverageException("No such option: %r" % option_name) 366