Sitelet https://github.com/coveragepy/coveragepy/pull/2263
Skip to content

fix: a non-string value in a TOML setting exits with a traceback - #2263

Merged
nedbat merged 1 commit into
coveragepy:mainfrom
VXNCXNX:fix/toml-non-string-values
Aug 18, 2026
Merged

nedbat merged 1 commit into
coveragepy:mainfrom
VXNCXNX:fix/toml-non-string-values

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

A non-string value in a TOML setting exits with a traceback instead of the
"Couldn't read config file" message coverage.py uses for every other config
error.

.coveragerc.toml containing [tool.coverage.paths] / source = ["src", 2]:

before:
Traceback (most recent call last):
  ...
  File ".../coverage/config.py", line 357, in from_file
    self.paths[option] = cp.getlist("paths", option)
  File ".../coverage/tomlconfig.py", line 191, in _get_list
    values = [substitute_variables(value, os.environ) for value in values]
  File ".../coverage/misc.py", line 284, in substitute_variables
    text = re.sub(dollar_pattern, dollar_replace, text)
TypeError: expected string or bytes-like object, got 'int'

after:
Couldn't read config file .coveragerc.toml: Option [tool.coverage.paths]source is not a list of strings: 2

Both exit 1. TOML makes this easy to hit by accident, since it has real integers
and booleans where the ini parser only ever produced strings.

Cause

Three gaps, all in the same shape:

  1. _get_list checks that the value is a list, then passes each element
    straight to substitute_variables, which calls re.sub on it. A non-string
    element raises TypeError from the re module. This reaches omit,
    exclude_lines, source, and every other list setting.
  2. getfile does not type-check at all, so data_file = 3 raises from
    os.path instead.
  3. [paths] is read outside the try/except ValueError in from_file that
    converts these into ConfigError, so even the existing checks escaped as
    tracebacks there.

The fix

Reuse the existing _check_type helper for list elements and for getfile, so
the message matches the ones the other settings already produce, and move the
[paths] loop inside the same try/except ValueError the rest of from_file
uses. No new error machinery.

Verification

test_toml_parse_errors gains 10 parametrized cases, each run against both
.coveragerc.toml and pyproject.toml.

Two mutations, because the change has two independent halves:

Reverting the tomlconfig.py guards fails 8 of them:

E       TypeError: expected string or bytes-like object, got 'int'
/usr/lib/python3.12/re/__init__.py:186: TypeError

Reverting only the config.py try/except fails the remaining 2:

E       ValueError: Option [tool.coverage.paths]source is not a list: 'not-a-list'
coverage/tomlconfig.py:174: ValueError

Both are exceptions escaping pytest.raises(ConfigError), not import or
collection errors.

tests/test_config.py goes from 88 passed to 98 passed, 6 skipped, 0 failed.
ruff format --check, ruff check and mypy are clean on the changed files.

On the rest of the suite: the failure set is byte-identical before and after
(122 failures both ways, diff of the sorted names is empty). They are
environmental here, no C extension and no network, and are present on unmodified
main. tests/test_venv.py was excluded for needing network. Only Python 3.12
is available on this machine.

Disclosure: written with AI assistance (Claude Code). I produced the before and after by running the real coverage CLI, and ran both mutation checks and the before/after failure-set comparison myself.

@nedbat
nedbat merged commit 0a5df25 into coveragepy:main Aug 18, 2026
72 checks passed
@nedbat

nedbat commented Aug 28, 2026

Copy link
Copy Markdown
Member

This is now released as part of coverage 7.16.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants