summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/backward.py15
-rw-r--r--coverage/cmdline.py4
-rw-r--r--coverage/config.py16
-rw-r--r--coverage/tomlconfig.py146
4 files changed, 176 insertions, 5 deletions
diff --git a/coverage/backward.py b/coverage/backward.py
index 58759545..e051fa55 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -6,6 +6,7 @@
# This file does tricky stuff, so disable a pylint warning.
# pylint: disable=unused-import
+import os
import sys
from coverage import env
@@ -26,6 +27,11 @@ try:
except ImportError:
import configparser
+try:
+ import toml
+except ImportError:
+ toml = None
+
# What's a string called?
try:
string_class = basestring
@@ -55,6 +61,15 @@ try:
except ImportError:
from threading import get_ident as get_thread_id
+try:
+ os.PathLike
+except AttributeError:
+ # This is Python 2 and 3
+ path_types = (bytes, string_class, unicode_class)
+else:
+ # 3.6+
+ path_types = (bytes, str, os.PathLike)
+
# shlex.quote is new, but there's an undocumented implementation in "pipes",
# who knew!?
try:
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index ef1184d0..448dd470 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -150,8 +150,8 @@ class Opts(object):
'', '--rcfile', action='store',
help=(
"Specify configuration file. "
- "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried. "
- "[env: COVERAGE_RCFILE]"
+ "By default '.coveragerc', 'pyproject.toml', 'setup.cfg' and "
+ "'tox.ini' are tried. [env: COVERAGE_RCFILE]"
),
)
source = optparse.make_option(
diff --git a/coverage/config.py b/coverage/config.py
index c6689d2d..89a0321e 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -9,10 +9,12 @@ import os
import re
from coverage import env
-from coverage.backward import configparser, iitems, string_class
+from coverage.backward import configparser, iitems, string_class, toml
from coverage.misc import contract, CoverageException, isolate_module
from coverage.misc import substitute_variables
+from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
+
os = isolate_module(os)
@@ -256,12 +258,19 @@ class CoverageConfig(object):
coverage.py settings in it.
"""
+ _, ext = os.path.splitext(filename)
+ if ext == '.toml':
+ if toml is None:
+ return False
+ cp = TomlConfigParser(our_file)
+ else:
+ cp = HandyConfigParser(our_file)
+
self.attempted_config_files.append(filename)
- cp = HandyConfigParser(our_file)
try:
files_read = cp.read(filename)
- except configparser.Error as err:
+ except (configparser.Error, TomlDecodeError) as err:
raise CoverageException("Couldn't read config file %s: %s" % (filename, err))
if not files_read:
return False
@@ -473,6 +482,7 @@ def config_files_to_try(config_file):
config_file = ".coveragerc"
files_to_try = [
(config_file, True, specified_file),
+ ("pyproject.toml", False, False),
("setup.cfg", False, False),
("tox.ini", False, False),
]
diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py
new file mode 100644
index 00000000..0d084603
--- /dev/null
+++ b/coverage/tomlconfig.py
@@ -0,0 +1,146 @@
+import io
+import os
+import re
+
+from coverage import env
+from coverage.backward import configparser, path_types, string_class, toml
+from coverage.misc import CoverageException, substitute_variables
+
+
+class TomlDecodeError(Exception):
+ """An exception class that exists even when toml isn't installed."""
+
+
+class TomlConfigParser:
+ def __init__(self, our_file):
+ self.getters = [lambda obj: obj['tool']['coverage']]
+ if our_file:
+ self.getters.append(lambda obj: obj)
+
+ self._data = []
+
+ def read(self, filenames):
+ if toml is None:
+ raise RuntimeError('toml module is not installed.')
+
+ if isinstance(filenames, path_types):
+ filenames = [filenames]
+ read_ok = []
+ for filename in filenames:
+ try:
+ with io.open(filename, encoding='utf-8') as fp:
+ self._data.append(toml.load(fp))
+ except IOError:
+ continue
+ except toml.TomlDecodeError as err:
+ raise TomlDecodeError(*err.args)
+ if env.PYVERSION >= (3, 6):
+ filename = os.fspath(filename)
+ read_ok.append(filename)
+ return read_ok
+
+ def has_option(self, section, option):
+ for data in self._data:
+ for getter in self.getters:
+ try:
+ getter(data)[section][option]
+ except KeyError:
+ continue
+ return True
+ return False
+
+ def has_section(self, section):
+ for data in self._data:
+ for getter in self.getters:
+ try:
+ getter(data)[section]
+ except KeyError:
+ continue
+ return section
+ return False
+
+ def options(self, section):
+ for data in self._data:
+ for getter in self.getters:
+ try:
+ section = getter(data)[section]
+ except KeyError:
+ continue
+ return list(section.keys())
+ raise configparser.NoSectionError(section)
+
+ def get_section(self, section):
+ d = {}
+ for opt in self.options(section):
+ d[opt] = self.get(section, opt)
+ return d
+
+ def get(self, section, option):
+ found_section = False
+ for data in self._data:
+ for getter in self.getters:
+ try:
+ section = getter(data)[section]
+ except KeyError:
+ continue
+
+ found_section = True
+ try:
+ value = section[option]
+ except KeyError:
+ continue
+ if isinstance(value, string_class):
+ value = substitute_variables(value, os.environ)
+ return value
+ if not found_section:
+ raise configparser.NoSectionError(section)
+ raise configparser.NoOptionError(option, section)
+
+ def getboolean(self, section, option):
+ value = self.get(section, option)
+ if not isinstance(value, bool):
+ raise ValueError(
+ 'Option {!r} in section {!r} is not a boolean: {!r}'
+ .format(option, section, value))
+ return value
+
+ def getlist(self, section, option):
+ values = self.get(section, option)
+ if not isinstance(values, list):
+ raise ValueError(
+ 'Option {!r} in section {!r} is not a list: {!r}'
+ .format(option, section, values))
+ for i, value in enumerate(values):
+ if isinstance(value, string_class):
+ values[i] = substitute_variables(value, os.environ)
+ return values
+
+ def getregexlist(self, section, option):
+ values = self.getlist(section, option)
+ for value in values:
+ value = value.strip()
+ try:
+ re.compile(value)
+ except re.error as e:
+ raise CoverageException(
+ "Invalid [%s].%s value %r: %s" % (section, option, value, e)
+ )
+ return values
+
+ def getint(self, section, option):
+ value = self.get(section, option)
+ if not isinstance(value, int):
+ raise ValueError(
+ 'Option {!r} in section {!r} is not an integer: {!r}'
+ .format(option, section, value))
+ return value
+
+ def getfloat(self, section, option):
+ value = self.get(section, option)
+ if isinstance(value, int):
+ value = float(value)
+ if not isinstance(value, float):
+ raise ValueError(
+ 'Option {!r} in section {!r} is not a float: {!r}'
+ .format(option, section, value))
+ return value