Skip to content

[WIP] Initial toml support #699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ Unreleased
plugins, but now it does, closing `issue 834`_.


- Added TOML configuration support, including pyproject.toml `issue 664`_.

.. _issue 720: https://github.com/nedbat/coveragepy/issues/720
.. _issue 822: https://github.com/nedbat/coveragepy/issues/822
.. _issue 834: https://github.com/nedbat/coveragepy/issues/834
.. _issue 829: https://github.com/nedbat/coveragepy/issues/829
.. _issue 846: https://github.com/nedbat/coveragepy/issues/846
.. _issue 664: https://github.com/nedbat/coveragepy/issues/664


.. _changes_50a6:
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Eduardo Schettino
Emil Madsen
Edward Loper
Federico Bond
Frazer McLean
Geoff Bache
George Paci
George Song
Expand Down
15 changes: 15 additions & 0 deletions coverage/backward.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +27,11 @@
except ImportError:
import configparser

try:
import toml
except ImportError:
toml = None

# What's a string called?
try:
string_class = basestring
Expand Down Expand Up @@ -55,6 +61,15 @@
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:
Expand Down
4 changes: 2 additions & 2 deletions coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,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(
Expand Down
16 changes: 13 additions & 3 deletions coverage/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
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)


Expand Down Expand Up @@ -255,12 +257,19 @@ def from_file(self, filename, our_file):
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
Expand Down Expand Up @@ -471,6 +480,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),
]
Expand Down
146 changes: 146 additions & 0 deletions coverage/tomlconfig.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have a case where our_file is True, so we could remove a little bit of code here.

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
5 changes: 4 additions & 1 deletion doc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ Coverage.py will read settings from other usual configuration files if no other
configuration file is used. It will automatically read from "setup.cfg" or
"tox.ini" if they exist. In this case, the section names have "coverage:"
prefixed, so the ``[run]`` options described below will be found in the
``[coverage:run]`` section of the file.
``[coverage:run]`` section of the file. If Coverage.py is installed with the
``toml`` extra (``pip install coverage[toml]``), it will automatically read
from "pyproject.toml". Configuration must be within the `[tool.coverage]`
section, e.g. ``[tool.coverage.run]`.


Syntax
Expand Down
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
],
},

extras_require={
# Enable pyproject.toml support
'toml': ['toml'],
},

# We need to get HTML assets from our htmlfiles directory.
zip_safe=False,

Expand Down
Loading