Oct 12, 2026 · v0.4.8

The Config Nobody Read

0.4.4 started reading pytest configuration. It read it with a line reader that could not reproduce pytest's parsing, and thirteen config shapes returned SAFE for a change the full suite fails. Configuration is now resolved by pytest's own code, and a config that cannot be read caps the verdict at UNPROVEN instead of being trusted.

0.4.4 closed a real defect: a pytest config file could narrow the suite invisibly and turn UNSAFE into SAFE. It closed it with a line reader that approximated pytest's parsing, and an approximation of a parser is a parser that is wrong somewhere. This release finds out where.

Where the reader was wrong

When the reader misread a file it reported "no narrowing", which is the answer that grants SAFE. A project whose own config deselected the one failing test could therefore receive a SAFE claiming the whole suite ran. Thirteen shapes were reproduced end to end, each returning SAFE for a change the full suite fails:

  • A multi-line TOML addopts string, and a black-style array with ] at column 0.
  • A comment after the [tool.pytest.ini_options] header, and a blank or comment line inside an ini value.
  • .pytest.ini, an ini continuation after a --x=y line, and a continuation line starting with [.
  • CR-only, form-feed and \x85 line separators, which the reader did not treat as line breaks.
  • A quoted "tool" table, a config named by -c in the test command, -c $VAR, the py.test spelling, and a diff that adds the filter to the config.

pytest reads its own config now

Configuration is resolved and parsed by pytest's own code, in a helper that reads a copy built the same way with the change applied. It honours pytest's precedence, -c / --config-file, -o / --override-ini, PYTEST_ADDOPTS, and pytest 9's pytest.toml, .pytest.toml and [tool.pytest]. It runs under the interpreter the test command names where that can be told, and because it cannot always be told, it works the answer out for each pytest generation and refuses when they would disagree.

This applies whatever else the command carries. Before, a flag Refactron does not recognise, such as --durations-min=0, made the scope unknown and skipped both the configuration and the PYTEST_ADDOPTS checks, so a narrowed run could return SAFE. That was true in 0.4.7 and earlier.

What can change for you

The rules that judge the values are unchanged with one exception: testpaths = [] is now full, because pytest ignores it, where it used to floor as unreadable. Most changes move verdicts toward UNPROVEN. Three correctly move to full, each because pytest really does run the whole suite: a black-style TOML array the old reader could not parse, testpaths = [], and a narrowing config in a file pytest does not select, such as a setup.cfg [tool:pytest] section when a pytest.ini exists.

Checked against pytest's own resolution on the config of 53 popular projects (58 file combinations) and on click and more-itertools at 20 sampled commits each. The values read were identical to pytest's everywhere pytest 9.1 could read them, and no scope changed on click or more-itertools. Scope did change where the old reader had not been reading the config at all: projects using pytest 9's native [tool.pytest] table (airflow, attrs, fastapi, pydantic, pytest, pytest-django) or dotted ini_options.* keys (tox) are now judged on what that config says.

Where the copies live

Every copy, shadow, baseline, retry, stability, mutation and coverage, is now created under a base directory owned by you with no group- or world-writable directory above it. The candidates, in order, are REFACTRON_VERIFY_BASE (the only one when set), os.tmpdir(), $XDG_CACHE_HOME/refactron or ~/.cache/refactron, the account home's .refactron/verify, then $XDG_RUNTIME_DIR. A copy that cannot be made ends the verdict UNPROVEN with coverage unknown, and never throws out of verifyDiff.

Before every suite spawn, the directories above the copy are checked for files the runner would read from there: pytest and Python config files, setup.py and conftest.py for Python commands, node_modules and tsconfig.json for JavaScript ones, and both when the command cannot be told. A hit ends the verdict UNPROVEN, naming the file and the directory it was found in. A refused mutation baseline is skipped and a refused mutant run is inconclusive, never killed.