Skip to content

Commit 0f071b7

Browse files
committed
fix: in toml config, only apply environment substitution to coverage settings. #1481
1 parent 615bdff commit 0f071b7

File tree

2 files changed

+49
-28
lines changed

2 files changed

+49
-28
lines changed

coverage/tomlconfig.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def read(self, filenames):
5252
except OSError:
5353
return []
5454
if tomllib is not None:
55-
toml_text = substitute_variables(toml_text, os.environ)
5655
try:
5756
self.data = tomllib.loads(toml_text)
5857
except tomllib.TOMLDecodeError as err:
@@ -101,9 +100,16 @@ def _get(self, section, option):
101100
if data is None:
102101
raise configparser.NoSectionError(section)
103102
try:
104-
return name, data[option]
103+
value = data[option]
105104
except KeyError as exc:
106105
raise configparser.NoOptionError(option, name) from exc
106+
return name, value
107+
108+
def _get_simple(self, section, option):
109+
name, value = self._get(section, option)
110+
if isinstance(value, str):
111+
value = substitute_variables(value, os.environ)
112+
return name, value
107113

108114
def has_option(self, section, option):
109115
_, data = self._get_section(section)
@@ -126,29 +132,40 @@ def get_section(self, section):
126132
return data
127133

128134
def get(self, section, option):
129-
_, value = self._get(section, option)
135+
_, value = self._get_simple(section, option)
130136
return value
131137

132-
def _check_type(self, section, option, value, type_, type_desc):
133-
if not isinstance(value, type_):
134-
raise ValueError(
135-
'Option {!r} in section {!r} is not {}: {!r}'
136-
.format(option, section, type_desc, value)
137-
)
138+
def _check_type(self, section, option, value, type_, converter, type_desc):
139+
if isinstance(value, type_):
140+
return value
141+
if isinstance(value, str) and converter is not None:
142+
try:
143+
return converter(value)
144+
except Exception:
145+
raise ValueError(
146+
f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}"
147+
) from None
148+
raise ValueError(
149+
f"Option [{section}]{option} is not {type_desc}: {value!r}"
150+
)
138151

139152
def getboolean(self, section, option):
140-
name, value = self._get(section, option)
141-
self._check_type(name, option, value, bool, "a boolean")
142-
return value
153+
name, value = self._get_simple(section, option)
154+
bool_strings = {"true": True, "false": False}
155+
return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean")
143156

144-
def getlist(self, section, option):
157+
def _getlist(self, section, option):
145158
name, values = self._get(section, option)
146-
self._check_type(name, option, values, list, "a list")
159+
values = self._check_type(name, option, values, list, None, "a list")
160+
values = [substitute_variables(value, os.environ) for value in values]
161+
return name, values
162+
163+
def getlist(self, section, option):
164+
_, values = self._getlist(section, option)
147165
return values
148166

149167
def getregexlist(self, section, option):
150-
name, values = self._get(section, option)
151-
self._check_type(name, option, values, list, "a list")
168+
name, values = self._getlist(section, option)
152169
for value in values:
153170
value = value.strip()
154171
try:
@@ -158,13 +175,11 @@ def getregexlist(self, section, option):
158175
return values
159176

160177
def getint(self, section, option):
161-
name, value = self._get(section, option)
162-
self._check_type(name, option, value, int, "an integer")
163-
return value
178+
name, value = self._get_simple(section, option)
179+
return self._check_type(name, option, value, int, int, "an integer")
164180

165181
def getfloat(self, section, option):
166-
name, value = self._get(section, option)
182+
name, value = self._get_simple(section, option)
167183
if isinstance(value, int):
168184
value = float(value)
169-
self._check_type(name, option, value, float, "a float")
170-
return value
185+
return self._check_type(name, option, value, float, float, "a float")

tests/test_config.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Test the config file handling for coverage.py"""
55

6-
import math
76
import sys
87
from collections import OrderedDict
98

@@ -89,7 +88,7 @@ def test_toml_config_file(self):
8988
assert cov.config.plugins == ["plugins.a_plugin"]
9089
assert cov.config.precision == 3
9190
assert cov.config.html_title == "tabblo & «ταБЬℓσ»"
92-
assert math.isclose(cov.config.fail_under, 90.5)
91+
assert cov.config.fail_under == 90.5
9392
assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"}
9493

9594
# Test that our class doesn't reject integers when loading floats
@@ -99,7 +98,7 @@ def test_toml_config_file(self):
9998
fail_under = 90
10099
""")
101100
cov = coverage.Coverage(config_file="pyproject.toml")
102-
assert math.isclose(cov.config.fail_under, 90)
101+
assert cov.config.fail_under == 90
103102
assert isinstance(cov.config.fail_under, float)
104103

105104
def test_ignored_config_file(self):
@@ -200,7 +199,7 @@ def test_parse_errors(self, bad_config, msg):
200199
r"multiple repeat"),
201200
('[tool.coverage.run]\nconcurrency="foo"', "not a list"),
202201
("[tool.coverage.report]\nprecision=1.23", "not an integer"),
203-
('[tool.coverage.report]\nfail_under="s"', "not a float"),
202+
('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"),
204203
])
205204
def test_toml_parse_errors(self, bad_config, msg):
206205
# Im-parsable values raise ConfigError, with details.
@@ -235,8 +234,10 @@ def test_environment_vars_in_toml_config(self):
235234
self.make_file("pyproject.toml", """\
236235
[tool.coverage.run]
237236
data_file = "$DATA_FILE.fooey"
238-
branch = $BRANCH
237+
branch = "$BRANCH"
239238
[tool.coverage.report]
239+
precision = "$DIGITS"
240+
fail_under = "$FAIL_UNDER"
240241
exclude_lines = [
241242
"the_$$one",
242243
"another${THING}",
@@ -245,15 +246,20 @@ def test_environment_vars_in_toml_config(self):
245246
"huh$${X}what",
246247
]
247248
[othersection]
249+
# This reproduces the failure from https://github.com/nedbat/coveragepy/issues/1481
250+
# When OTHER has a backslash that isn't a valid escape, like \\z (see below).
248251
something = "if [ $OTHER ]; then printf '%s\\n' 'Hi'; fi"
249252
""")
250253
self.set_environ("BRANCH", "true")
254+
self.set_environ("DIGITS", "3")
255+
self.set_environ("FAIL_UNDER", "90.5")
251256
self.set_environ("DATA_FILE", "hello-world")
252257
self.set_environ("THING", "ZZZ")
253258
self.set_environ("OTHER", "hi\\zebra")
254259
cov = coverage.Coverage()
255-
assert cov.config.data_file == "hello-world.fooey"
256260
assert cov.config.branch is True
261+
assert cov.config.precision == 3
262+
assert cov.config.data_file == "hello-world.fooey"
257263
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
258264

259265
def test_tilde_in_config(self):

0 commit comments

Comments
 (0)