1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
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 |
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
3 |
3 |
4 """TOML configuration support for coverage.py""" |
4 """TOML configuration support for coverage.py""" |
5 |
5 |
6 import io |
6 import configparser |
7 import os |
7 import os |
8 import re |
8 import re |
9 |
9 |
10 from coverage import env |
10 from coverage.exceptions import CoverageException |
11 from coverage.backward import configparser, path_types |
11 from coverage.misc import import_third_party, substitute_variables |
12 from coverage.misc import CoverageException, substitute_variables |
|
13 |
12 |
14 # TOML support is an install-time extra option. |
13 # TOML support is an install-time extra option. (Import typing is here because |
15 try: |
14 # import_third_party will unload any module that wasn't already imported. |
16 import toml |
15 # tomli imports typing, and if we unload it, later it's imported again, and on |
17 except ImportError: # pragma: not covered |
16 # Python 3.6, this causes infinite recursion.) |
18 toml = None |
17 import typing # pylint: disable=unused-import, wrong-import-order |
|
18 tomli = import_third_party("tomli") |
19 |
19 |
20 |
20 |
21 class TomlDecodeError(Exception): |
21 class TomlDecodeError(Exception): |
22 """An exception class that exists even when toml isn't installed.""" |
22 """An exception class that exists even when toml isn't installed.""" |
23 pass |
23 pass |
35 self.data = None |
35 self.data = None |
36 |
36 |
37 def read(self, filenames): |
37 def read(self, filenames): |
38 # RawConfigParser takes a filename or list of filenames, but we only |
38 # RawConfigParser takes a filename or list of filenames, but we only |
39 # ever call this with a single filename. |
39 # ever call this with a single filename. |
40 assert isinstance(filenames, path_types) |
40 assert isinstance(filenames, (bytes, str, os.PathLike)) |
41 filename = filenames |
41 filename = os.fspath(filenames) |
42 if env.PYVERSION >= (3, 6): |
|
43 filename = os.fspath(filename) |
|
44 |
42 |
45 try: |
43 try: |
46 with io.open(filename, encoding='utf-8') as fp: |
44 with open(filename, encoding='utf-8') as fp: |
47 toml_text = fp.read() |
45 toml_text = fp.read() |
48 except IOError: |
46 except OSError: |
49 return [] |
47 return [] |
50 if toml: |
48 if tomli is not None: |
51 toml_text = substitute_variables(toml_text, os.environ) |
49 toml_text = substitute_variables(toml_text, os.environ) |
52 try: |
50 try: |
53 self.data = toml.loads(toml_text) |
51 self.data = tomli.loads(toml_text) |
54 except toml.TomlDecodeError as err: |
52 except tomli.TOMLDecodeError as err: |
55 raise TomlDecodeError(*err.args) |
53 raise TomlDecodeError(str(err)) from err |
56 return [filename] |
54 return [filename] |
57 else: |
55 else: |
58 has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE) |
56 has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE) |
59 if self.our_file or has_toml: |
57 if self.our_file or has_toml: |
60 # Looks like they meant to read TOML, but we can't read it. |
58 # Looks like they meant to read TOML, but we can't read it. |
96 name, data = self._get_section(section) |
94 name, data = self._get_section(section) |
97 if data is None: |
95 if data is None: |
98 raise configparser.NoSectionError(section) |
96 raise configparser.NoSectionError(section) |
99 try: |
97 try: |
100 return name, data[option] |
98 return name, data[option] |
101 except KeyError: |
99 except KeyError as exc: |
102 raise configparser.NoOptionError(option, name) |
100 raise configparser.NoOptionError(option, name) from exc |
103 |
101 |
104 def has_option(self, section, option): |
102 def has_option(self, section, option): |
105 _, data = self._get_section(section) |
103 _, data = self._get_section(section) |
106 if data is None: |
104 if data is None: |
107 return False |
105 return False |
148 for value in values: |
146 for value in values: |
149 value = value.strip() |
147 value = value.strip() |
150 try: |
148 try: |
151 re.compile(value) |
149 re.compile(value) |
152 except re.error as e: |
150 except re.error as e: |
153 raise CoverageException( |
151 raise CoverageException(f"Invalid [{name}].{option} value {value!r}: {e}") from e |
154 "Invalid [%s].%s value %r: %s" % (name, option, value, e) |
|
155 ) |
|
156 return values |
152 return values |
157 |
153 |
158 def getint(self, section, option): |
154 def getint(self, section, option): |
159 name, value = self._get(section, option) |
155 name, value = self._get(section, option) |
160 self._check_type(name, option, value, int, "an integer") |
156 self._check_type(name, option, value, int, "an integer") |