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