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 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, 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. |
|
124 DEFAULT_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. |
|
129 DEFAULT_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. |
|
136 DEFAULT_PARTIAL_ALWAYS = [ |
|
137 'while (True|1|False|0):', |
|
138 'if (True|1|False|0):', |
|
139 ] |
|
140 |
|
141 |
|
142 class 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 |
|
367 # |
|
368 # eflag: FileType = Python2 |
|