summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorFrazer McLean <frazer@frazermclean.co.uk>2019-09-16 22:24:53 +0200
committerNed Batchelder <ned@nedbatchelder.com>2019-11-03 21:27:42 -0500
commitf97d0750a91e53bec387528344c1ca3bf86e1d08 (patch)
treeb240becda198190abdf545f02ce116ca3eae8808 /coverage
parent1c06204a8b1db6cd5f53c553c42f3ef8229d6b20 (diff)
downloadpython-coveragepy-git-f97d0750a91e53bec387528344c1ca3bf86e1d08.tar.gz
TOML support for pyproject.toml and other config files
Squashed and rebased from https://github.com/nedbat/coveragepy/pull/699 Missing getfloat TOMLConfigParser -> TomlConfigParser fix getfloat for int Move TomlConfigParser Add name to contributors Import toml in backward.py fix indentation Don't ignore TomlDecodeError Raise if TomlConfigParser is used without toml installed Add tests for TOML config Fix test on Python 2 Mention toml support in documentation.
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