|
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 """TOML configuration support for coverage.py""" |
|
5 |
|
6 import io |
|
7 import os |
|
8 import re |
|
9 |
|
10 from coverage import env |
|
11 from coverage.backward import configparser, path_types |
|
12 from coverage.misc import CoverageException, substitute_variables |
|
13 |
|
14 |
|
15 class TomlDecodeError(Exception): |
|
16 """An exception class that exists even when toml isn't installed.""" |
|
17 pass |
|
18 |
|
19 |
|
20 class TomlConfigParser: |
|
21 """TOML file reading with the interface of HandyConfigParser.""" |
|
22 |
|
23 # This class has the same interface as config.HandyConfigParser, no |
|
24 # need for docstrings. |
|
25 # pylint: disable=missing-function-docstring |
|
26 |
|
27 def __init__(self, our_file): |
|
28 self.our_file = our_file |
|
29 self.data = None |
|
30 |
|
31 def read(self, filenames): |
|
32 from coverage.optional import toml |
|
33 |
|
34 # RawConfigParser takes a filename or list of filenames, but we only |
|
35 # ever call this with a single filename. |
|
36 assert isinstance(filenames, path_types) |
|
37 filename = filenames |
|
38 if env.PYVERSION >= (3, 6): |
|
39 filename = os.fspath(filename) |
|
40 |
|
41 try: |
|
42 with io.open(filename, encoding='utf-8') as fp: |
|
43 toml_text = fp.read() |
|
44 except IOError: |
|
45 return [] |
|
46 if toml: |
|
47 toml_text = substitute_variables(toml_text, os.environ) |
|
48 try: |
|
49 self.data = toml.loads(toml_text) |
|
50 except toml.TomlDecodeError as err: |
|
51 raise TomlDecodeError(*err.args) |
|
52 return [filename] |
|
53 else: |
|
54 has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE) |
|
55 if self.our_file or has_toml: |
|
56 # Looks like they meant to read TOML, but we can't read it. |
|
57 msg = "Can't read {!r} without TOML support. Install with [toml] extra" |
|
58 raise CoverageException(msg.format(filename)) |
|
59 return [] |
|
60 |
|
61 def _get_section(self, section): |
|
62 """Get a section from the data. |
|
63 |
|
64 Arguments: |
|
65 section (str): A section name, which can be dotted. |
|
66 |
|
67 Returns: |
|
68 name (str): the actual name of the section that was found, if any, |
|
69 or None. |
|
70 data (str): the dict of data in the section, or None if not found. |
|
71 |
|
72 """ |
|
73 prefixes = ["tool.coverage."] |
|
74 if self.our_file: |
|
75 prefixes.append("") |
|
76 for prefix in prefixes: |
|
77 real_section = prefix + section |
|
78 parts = real_section.split(".") |
|
79 try: |
|
80 data = self.data[parts[0]] |
|
81 for part in parts[1:]: |
|
82 data = data[part] |
|
83 except KeyError: |
|
84 continue |
|
85 break |
|
86 else: |
|
87 return None, None |
|
88 return real_section, data |
|
89 |
|
90 def _get(self, section, option): |
|
91 """Like .get, but returns the real section name and the value.""" |
|
92 name, data = self._get_section(section) |
|
93 if data is None: |
|
94 raise configparser.NoSectionError(section) |
|
95 try: |
|
96 return name, data[option] |
|
97 except KeyError: |
|
98 raise configparser.NoOptionError(option, name) |
|
99 |
|
100 def has_option(self, section, option): |
|
101 _, data = self._get_section(section) |
|
102 if data is None: |
|
103 return False |
|
104 return option in data |
|
105 |
|
106 def has_section(self, section): |
|
107 name, _ = self._get_section(section) |
|
108 return name |
|
109 |
|
110 def options(self, section): |
|
111 _, data = self._get_section(section) |
|
112 if data is None: |
|
113 raise configparser.NoSectionError(section) |
|
114 return list(data.keys()) |
|
115 |
|
116 def get_section(self, section): |
|
117 _, data = self._get_section(section) |
|
118 return data |
|
119 |
|
120 def get(self, section, option): |
|
121 _, value = self._get(section, option) |
|
122 return value |
|
123 |
|
124 def _check_type(self, section, option, value, type_, type_desc): |
|
125 if not isinstance(value, type_): |
|
126 raise ValueError( |
|
127 'Option {!r} in section {!r} is not {}: {!r}' |
|
128 .format(option, section, type_desc, value) |
|
129 ) |
|
130 |
|
131 def getboolean(self, section, option): |
|
132 name, value = self._get(section, option) |
|
133 self._check_type(name, option, value, bool, "a boolean") |
|
134 return value |
|
135 |
|
136 def getlist(self, section, option): |
|
137 name, values = self._get(section, option) |
|
138 self._check_type(name, option, values, list, "a list") |
|
139 return values |
|
140 |
|
141 def getregexlist(self, section, option): |
|
142 name, values = self._get(section, option) |
|
143 self._check_type(name, option, values, list, "a list") |
|
144 for value in values: |
|
145 value = value.strip() |
|
146 try: |
|
147 re.compile(value) |
|
148 except re.error as e: |
|
149 raise CoverageException( |
|
150 "Invalid [%s].%s value %r: %s" % (name, option, value, e) |
|
151 ) |
|
152 return values |
|
153 |
|
154 def getint(self, section, option): |
|
155 name, value = self._get(section, option) |
|
156 self._check_type(name, option, value, int, "an integer") |
|
157 return value |
|
158 |
|
159 def getfloat(self, section, option): |
|
160 name, value = self._get(section, option) |
|
161 if isinstance(value, int): |
|
162 value = float(value) |
|
163 self._check_type(name, option, value, float, "a float") |
|
164 return value |