summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2019-11-04 05:50:04 -0500
committerNed Batchelder <ned@nedbatchelder.com>2019-11-04 07:17:19 -0500
commit15f2ffed0113f9642275356c4dfac1ba45d8a74b (patch)
treeb4694d43ffdec90e8e07a32a433f8e9017486565
parentdf744f8cbcad7ea7dca893be5017920afa4ce32f (diff)
downloadpython-coveragepy-git-15f2ffed0113f9642275356c4dfac1ba45d8a74b.tar.gz
Refactor the toml logic
- Section names can be dotted. - We only ever read one file, so we don't need to loop over files. - Error messages should show the actual section names where problems happened.
-rw-r--r--coverage/tomlconfig.py159
-rw-r--r--tests/test_config.py27
2 files changed, 103 insertions, 83 deletions
diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py
index f5978820..cf1d82b1 100644
--- a/coverage/tomlconfig.py
+++ b/coverage/tomlconfig.py
@@ -26,13 +26,11 @@ class TomlConfigParser:
def __init__(self, our_file):
self.our_file = our_file
- self.getters = [lambda obj: obj['tool']['coverage']]
- if self.our_file:
- self.getters.append(lambda obj: obj)
-
- self._data = []
+ 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)
@@ -40,130 +38,137 @@ class TomlConfigParser:
if env.PYVERSION >= (3, 6):
filename = os.fspath(filename)
- from coverage.optional import toml
- if toml is None:
- if self.our_file:
- raise CoverageException("Can't read {!r} without TOML support".format(filename))
-
try:
with io.open(filename, encoding='utf-8') as fp:
- toml_data = fp.read()
- toml_data = substitute_variables(toml_data, os.environ)
- if toml:
- try:
- self._data.append(toml.loads(toml_data))
- except toml.TomlDecodeError as err:
- raise TomlDecodeError(*err.args)
- elif re.search(r"^\[tool\.coverage\.", toml_data, flags=re.MULTILINE):
- # Looks like they meant to read TOML, but we can't.
- raise CoverageException("Can't read {!r} without TOML support".format(filename))
- else:
- return []
+ toml_text = fp.read()
except IOError:
return []
- return [filename]
+ 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):
- for data in self._data:
- for getter in self.getters:
- try:
- getter(data)[section][option]
- except KeyError:
- continue
- return True
- return False
+ _, data = self._get_section(section)
+ if data is None:
+ return False
+ return option in data
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
+ name, _ = self._get_section(section)
+ return name
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)
+ _, data = self._get_section(section)
+ if data is None:
+ raise configparser.NoSectionError(section)
+ return list(data.keys())
def get_section(self, section):
- d = {}
- for opt in self.options(section):
- d[opt] = self.get(section, opt)
- return d
+ _, data = self._get_section(section)
+ return data
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
- return value
- if not found_section:
- raise configparser.NoSectionError(section)
- raise configparser.NoOptionError(option, section)
+ _, value = self._get(section, option)
+ return value
def getboolean(self, section, option):
- value = self.get(section, option)
+ name, 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)
+ .format(option, name, value)
)
return value
def getlist(self, section, option):
- values = self.get(section, option)
+ name, 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)
+ .format(option, name, values)
)
return values
def getregexlist(self, section, option):
+ # Use getlist for list-checking.
values = self.getlist(section, option)
+ name, values = self._get(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)
+ "Invalid [%s].%s value %r: %s" % (name, option, value, e)
)
return values
def getint(self, section, option):
- value = self.get(section, option)
+ name, 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)
+ .format(option, name, value)
)
return value
def getfloat(self, section, option):
- value = self.get(section, option)
+ name, 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)
+ .format(option, name, value)
)
return value
diff --git a/tests/test_config.py b/tests/test_config.py
index 74ff5f00..0f9a4929 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -65,17 +65,25 @@ class ConfigTest(CoverageTest):
concurrency = ["a", "b"]
timid = true
data_file = ".hello_kitty.data"
+ plugins = ["plugins.a_plugin"]
[tool.coverage.report]
precision = 3
fail_under = 90.5
+ [tool.coverage.plugins.a_plugin]
+ hello = "world"
""")
cov = coverage.Coverage(config_file="pyproject.toml")
self.assertTrue(cov.config.timid)
self.assertFalse(cov.config.branch)
self.assertEqual(cov.config.concurrency, ["a", "b"])
self.assertEqual(cov.config.data_file, ".hello_kitty.data")
+ self.assertEqual(cov.config.plugins, ["plugins.a_plugin"])
self.assertEqual(cov.config.precision, 3)
self.assertAlmostEqual(cov.config.fail_under, 90.5)
+ self.assertEqual(
+ cov.config.get_plugin_options("plugins.a_plugin"),
+ {'hello': 'world'}
+ )
# Test that our class doesn't reject integers when loading floats
self.make_file("pyproject.toml", """\
@@ -153,7 +161,7 @@ class ConfigTest(CoverageTest):
# Im-parsable values raise CoverageException, with details.
bad_configs_and_msgs = [
("[run]\ntimid = maybe?\n", r"maybe[?]"),
- ("timid = 1\n", r"timid = 1"),
+ ("timid = 1\n", r"no section headers"),
("[run\n", r"\[run"),
("[report]\nexclude_lines = foo(\n",
r"Invalid \[report\].exclude_lines value 'foo\(': "
@@ -177,16 +185,15 @@ class ConfigTest(CoverageTest):
# Im-parsable values raise CoverageException, with details.
bad_configs_and_msgs = [
("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"),
- # ("timid = 1\n", r"timid = 1"),
("[tool.coverage.run\n", r"Key group"),
('[tool.coverage.report]\nexclude_lines = ["foo("]\n',
- r"Invalid \[report\].exclude_lines value u?'foo\(': "
+ r"Invalid \[tool.coverage.report\].exclude_lines value u?'foo\(': "
r"(unbalanced parenthesis|missing \))"),
('[tool.coverage.report]\npartial_branches = ["foo["]\n',
- r"Invalid \[report\].partial_branches value u?'foo\[': "
+ r"Invalid \[tool.coverage.report\].partial_branches value u?'foo\[': "
r"(unexpected end of regular expression|unterminated character set)"),
('[tool.coverage.report]\npartial_branches_always = ["foo***"]\n',
- r"Invalid \[report\].partial_branches_always value "
+ r"Invalid \[tool.coverage.report\].partial_branches_always value "
r"u?'foo\*\*\*': "
r"multiple repeat"),
('[tool.coverage.run]\nconcurrency="foo"', "not a list"),
@@ -367,7 +374,7 @@ class ConfigTest(CoverageTest):
[tool.coverage.run]
xyzzy = 17
""")
- msg = r"Unrecognized option '\[run\] xyzzy=' in config file pyproject.toml"
+ msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml"
with self.assertRaisesRegex(CoverageException, msg):
_ = coverage.Coverage()
@@ -653,8 +660,16 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest):
self.assertFalse(cov.config.branch)
self.assertEqual(cov.config.data_file, ".coverage")
+ def test_no_toml_installed_no_toml(self):
+ # Can't read a toml file that doesn't exist.
+ with coverage.optional.without('toml'):
+ msg = "Couldn't read 'cov.toml' as a config file"
+ with self.assertRaisesRegex(CoverageException, msg):
+ coverage.Coverage(config_file="cov.toml")
+
def test_no_toml_installed_explicit_toml(self):
# Can't specify a toml config file if toml isn't installed.
+ self.make_file("cov.toml", "# A toml file!")
with coverage.optional.without('toml'):
msg = "Can't read 'cov.toml' without TOML support"
with self.assertRaisesRegex(CoverageException, msg):