Skip to content

Commit 4fe7c61

Browse files
committed
Strict variable substitution is now an option
1 parent dde0a3e commit 4fe7c61

File tree

4 files changed

+37
-11
lines changed

4 files changed

+37
-11
lines changed

CHANGES.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ Change history for Coverage.py
1717
Unreleased
1818
----------
1919

20-
- nothing yet.
20+
- Environment variable substitution in configuration files can now be strict:
21+
using a question mark suffix like ``${VARNAME?}`` will raise an error if
22+
``VARNAME`` is not defined as an environment variable.
2123

2224

2325
.. _changes_50a2:

coverage/misc.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def substitute_variables(text, variables=os.environ):
258258
259259
$VAR
260260
${VAR}
261+
${VAR?} strict: an error if VAR isn't defined.
261262
262263
A dollar can be inserted with ``$$``.
263264
@@ -270,16 +271,24 @@ def substitute_variables(text, variables=os.environ):
270271
def dollar_replace(m):
271272
"""Called for each $replacement."""
272273
# Only one of the groups will have matched, just get its text.
273-
word = next(w for w in m.groups() if w is not None) # pragma: part covered
274+
word = m.expand(r"\g<v1>\g<v2>\g<char>")
274275
if word == "$":
275276
return "$"
276277
else:
278+
strict = bool(m.group('strict'))
279+
if strict:
280+
if word not in variables:
281+
msg = "Variable {} is undefined: {}".format(word, text)
282+
raise CoverageException(msg)
277283
return variables.get(word, '')
278284

279285
dollar_pattern = r"""(?x) # Use extended regex syntax
280286
\$(?: # A dollar sign, then
281287
(?P<v1>\w+) | # a plain word,
282-
{(?P<v2>\w+)} | # or a {-wrapped word,
288+
{ # or a {-wrapped word,
289+
(?P<v2>\w+)
290+
(?P<strict>\??) # with maybe a strict marker
291+
} |
283292
(?P<char>[$]) # or a dollar sign.
284293
)
285294
"""

doc/config.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ or ``0`` and are case-insensitive.
5959

6060
Environment variables can be substituted in by using dollar signs: ``$WORD``
6161
or ``${WORD}`` will be replaced with the value of ``WORD`` in the environment.
62-
A dollar sign can be inserted with ``$$``. Missing environment variables
63-
will result in empty strings with no error.
62+
A dollar sign can be inserted with ``$$``. If you want to raise an error if
63+
an environment variable is undefined, use a question mark suffix: ``${WORD?}``.
64+
Otherwise, missing environment variables will result in empty strings with no
65+
error.
6466

6567
Many sections and values correspond roughly to commands and options in
6668
the :ref:`command-line interface <cmd>`.

tests/test_misc.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from coverage.misc import contract, dummy_decorator_with_args, file_be_gone
99
from coverage.misc import format_lines, Hasher, one_of, substitute_variables
10+
from coverage.misc import CoverageException
1011

1112
from tests.coveragetest import CoverageTest
1213

@@ -137,17 +138,29 @@ def test_format_lines(statements, lines, result):
137138
assert format_lines(statements, lines) == result
138139

139140

141+
VARS = {
142+
'FOO': 'fooey',
143+
'BAR': 'xyzzy',
144+
}
145+
140146
@pytest.mark.parametrize("before, after", [
141147
("Nothing to do", "Nothing to do"),
142148
("Dollar: $$", "Dollar: $"),
143149
("Simple: $FOO is fooey", "Simple: fooey is fooey"),
144150
("Braced: X${FOO}X.", "Braced: XfooeyX."),
145-
("Missing: x$NOTHING is x", "Missing: x is x"),
151+
("Missing: x${NOTHING}y is xy", "Missing: xy is xy"),
146152
("Multiple: $$ $FOO $BAR ${FOO}", "Multiple: $ fooey xyzzy fooey"),
153+
("Ill-formed: ${%5} ${{HI}} ${", "Ill-formed: ${%5} ${{HI}} ${"),
154+
("Strict: ${FOO?} is there", "Strict: fooey is there"),
147155
])
148156
def test_substitute_variables(before, after):
149-
variables = {
150-
'FOO': 'fooey',
151-
'BAR': 'xyzzy',
152-
}
153-
assert substitute_variables(before, variables) == after
157+
assert substitute_variables(before, VARS) == after
158+
159+
@pytest.mark.parametrize("text", [
160+
"Strict: ${NOTHING?} is an error",
161+
])
162+
def test_substitute_variables_errors(text):
163+
with pytest.raises(CoverageException) as exc_info:
164+
substitute_variables(text, VARS)
165+
assert text in str(exc_info.value)
166+
assert "Variable NOTHING is undefined" in str(exc_info.value)

0 commit comments

Comments
 (0)