blob: 25542f99ef8c365cf77f4cd5f3fac53b8b9e8b77 [file] [log] [blame]
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""TOML configuration support for coverage.py"""
import io
import os
import re
from coverage import env
from coverage.backward import configparser, path_types
from coverage.misc import CoverageException, substitute_variables
class TomlDecodeError(Exception):
"""An exception class that exists even when toml isn't installed."""
pass
class TomlConfigParser:
"""TOML file reading with the interface of HandyConfigParser."""
# This class has the same interface as config.HandyConfigParser, no
# need for docstrings.
# pylint: disable=missing-function-docstring
def __init__(self, our_file):
self.our_file = our_file
self.data = None
def read(self, filenames):
from coverage.optional import toml
# RawConfigParser takes a filename or list of filenames, but we only
# ever call this with a single filename.
assert isinstance(filenames, path_types)
filename = filenames
if env.PYVERSION >= (3, 6):
filename = os.fspath(filename)
try:
with io.open(filename, encoding='utf-8') as fp:
toml_text = fp.read()
except IOError:
return []
if toml:
toml_text = substitute_variables(toml_text, os.environ)
try:
self.data = toml.loads(toml_text)
except toml.TomlDecodeError as err:
raise TomlDecodeError(*err.args)
return [filename]
else:
has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE)
if self.our_file or has_toml:
# Looks like they meant to read TOML, but we can't read it.
msg = "Can't read {!r} without TOML support. Install with [toml] extra"
raise CoverageException(msg.format(filename))
return []
def _get_section(self, section):
"""Get a section from the data.
Arguments:
section (str): A section name, which can be dotted.
Returns:
name (str): the actual name of the section that was found, if any,
or None.
data (str): the dict of data in the section, or None if not found.
"""
prefixes = ["tool.coverage."]
if self.our_file:
prefixes.append("")
for prefix in prefixes:
real_section = prefix + section
parts = real_section.split(".")
try:
data = self.data[parts[0]]
for part in parts[1:]:
data = data[part]
except KeyError:
continue
break
else:
return None, None
return real_section, data
def _get(self, section, option):
"""Like .get, but returns the real section name and the value."""
name, data = self._get_section(section)
if data is None:
raise configparser.NoSectionError(section)
try:
return name, data[option]
except KeyError:
raise configparser.NoOptionError(option, name)
def has_option(self, section, option):
_, data = self._get_section(section)
if data is None:
return False
return option in data
def has_section(self, section):
name, _ = self._get_section(section)
return name
def options(self, section):
_, data = self._get_section(section)
if data is None:
raise configparser.NoSectionError(section)
return list(data.keys())
def get_section(self, section):
_, data = self._get_section(section)
return data
def get(self, section, option):
_, value = self._get(section, option)
return value
def _check_type(self, section, option, value, type_, type_desc):
if not isinstance(value, type_):
raise ValueError(
'Option {!r} in section {!r} is not {}: {!r}'
.format(option, section, type_desc, value)
)
def getboolean(self, section, option):
name, value = self._get(section, option)
self._check_type(name, option, value, bool, "a boolean")
return value
def getlist(self, section, option):
name, values = self._get(section, option)
self._check_type(name, option, values, list, "a list")
return values
def getregexlist(self, section, option):
name, values = self._get(section, option)
self._check_type(name, option, values, list, "a list")
for value in values:
value = value.strip()
try:
re.compile(value)
except re.error as e:
raise CoverageException(
"Invalid [%s].%s value %r: %s" % (name, option, value, e)
)
return values
def getint(self, section, option):
name, value = self._get(section, option)
self._check_type(name, option, value, int, "an integer")
return value
def getfloat(self, section, option):
name, value = self._get(section, option)
if isinstance(value, int):
value = float(value)
self._check_type(name, option, value, float, "a float")
return value